Successfully reported this slideshow.
Your SlideShare is downloading. ×

Jbatch実践入門 #jdt2015

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 73 Ad
Advertisement

More Related Content

Slideshows for you (20)

Viewers also liked (18)

Advertisement

Similar to Jbatch実践入門 #jdt2015 (20)

Advertisement

Recently uploaded (20)

Jbatch実践入門 #jdt2015

  1. 1. jBatch実践入門 NTTコムウェア株式会社 上妻 宜人 (あげつま のりと) はてなブログ : 見習いプログラミング日記 【Java Day Tokyo 2015 6-5】
  2. 2. 上妻 宜人 あげつま のりと • Javaトラブル解析/技術サポート業務に従事 • Java EE について調べて伝えることが好き • 日本Javaユーザグループでの講演 • JJUG CCC 2014, ナイトセミナ, JavaOne報告会 など
  3. 3. 『バッチジョブ』 どのように実装していますか?
  4. 4. よくあるバッチアプリ構成を 思い浮かべてみます。
  5. 5. よくあるバッチ構成例 ジョブスケジューラ ジョブネットA Job1 Job2-1 Job2-2 Job3 マシンA Job1 Job2 Job2 Job3 mainメソッドから始まるJavaプログラム マシンB
  6. 6. ジョブスケジューラ ジョブネットA Job1 Job2-1 Job2-2 Job3 マシンA Job1 Job2 Job2 Job3 mainメソッドから始まるJavaプログラム マシンB • 試験期間の後半に問題が見つかる • 一度に大量ロードしてOutOfMemoryError • JDBCのexecuteBatch未使用による性能遅延 • 似たようなジョブでも構造がばらばら • 独自フレームワークは難読化と隣り合わせ よくある困ったジョブ実装
  7. 7. ジョブ実装への様々な要求 • 耐エラー性 • 不正データが混在していても弾いて準正常終了 • リスタート可能 • エラーによる再実行時には途中から再開可 • 多様な入出力形式 • CSVファイルやRDBMSが入力とは限らない • Web API によりjson形式の入力を持ってくる 等
  8. 8. これら課題を解決するための、 1つの候補が Java EE7より導入 (2013/5 release)
  9. 9. JSR 352 Batch Application for The Java Platform
  10. 10. 本日のコンテンツ • jBatchとは何か • jBatchをどうやって使うのか • メリット と デメリット • よくある疑問 • まとめ
  11. 11. jBatchとは何か? • Java EE7 より導入されたバッチフレームワーク • 各ジョブの実装を楽にするのが目的 • Spring Batch の構造を引き継いで仕様化
  12. 12. jBatchの全体像 1. バッチ用語とアーキテクチャの定義 JobOperator Job Item Processor Item Writer 2. ジョブXML 3. ユーザが実装するAPI <job id=“monthly_billing”> <step id=“step1” next=“step2”> <step id=“step2”> </job> Step1 Step2 Step3 Job Repository Item Reader public interface ItemReader { Object readItem() throws ... }
  13. 13. jBatch機能 #1 順序制御 ジョブスケジューラ Job1 job2 Cron or EJBタイマ 0 4 * * * batch.sh jBatch Job Step1 Step2 Step4 Decision (条件分岐) Step3-1 Step3-2Split (並行実行) Job XML </> ジョブフローはXML、ステップや分岐条件はJavaで実装 Java Java Java Java Java
  14. 14. jBatch機能#2 処理のフレームワーク化 <<interface>> Item Processor <<interface>> Item Writer Step1 <<interface>> Item Reader • バッチ処理の多くは 『入力⇒処理⇒出力』 の流れ • 入出力と処理を分けて作成し、jBatchが呼び出す構造 • フレームワークによる設計の再利用 jBatch Runtime readItem() jBatch Job Java Java Java processItem() writeItems() Step2
  15. 15. jBatch機能#3 チェックポイント • 異常終了に備えてチェックポイント情報を収集 • デフォルト 10アイテム処理間隔 • リスタート時にItemReaderとItemWriterに渡す • 例えば、ファイルの途中から再処理が可能 Job Repository 10 30 ジョブ 完了 10行完了 COMIT 20 COMIT ×20行完了 restart 20行目から再処理 start
  16. 16. jBatch機能#4 エラーハンドリング • スキップ • 例外が投げられたレコードの処理をスキップ • 例: 入力チェックエラー行はロギングのちスキップ • 上限を設定して、ジョブ自体の異常終了も可能 • リトライ • 例外が投げられたら該当のステップを自動リトライ • デフォルトはリトライなし。ジョブ異常終了。
  17. 17. jBatchのアーキテクチャ JobOperator Job Item Processor Item Writer Step1 Step2 Step3 Job Repository Item Reader • JobOperatorからジョブ起動 • 1つのジョブは複数のステップより構成 • 各ステップは Read – Process – Write に分けて実装 • 起動終了時刻、終了ステータスをJobRepositoryに保存
  18. 18. ジョブ 請求書発行ジョブ 【Step1】 請求額確定 【Step2】 PDF生成 【Step3】 メール送付 • トップレベル要素 • jBatchにジョブネットやジョブグループのような考え方はない • ジョブは必ず1つ以上のステップを含む • パラメータ、再実行可否 を設定可能 • param : /var/file/売り上げデータ.csv • エラー時は途中ステップから再実行可 (例: メールサーバ異常時には、Step3から再実行) (デフォルト再実行可)
  19. 19. ステップ • ユーザがJavaで実装する処理 • ステップ毎に2種類の実装方式が選べる • チャンク: Reader, Processor, Writer を分けて実装 • バッチレット: 単純に1回だけ呼ばれるタスク
  20. 20. ステップ: チャンク方式 • ItemReader、ItemProcessor、ItemWriter の3つを実装 • 入力と処理を繰り返した後、一定数まとめて出力 • ファイルやDBなどのレコード単位の処理向け Item Processor Item Writer Step Item Reader jBatch Runtime read process 『入力 ⇒ 処理』 1レコードずつ 10回繰り返す Commit ▶ write10レコード分まとめて書き出す
  21. 21. ステップ: チャンク方式のメリット • バッチ処理の一定の型を規定する • 似たような処理を一つの設計にまとめる • Reader / Writer を汎用ライブラリ化しやすい Item Processor Item Writer Item Reader • CSVなどの FlatFileReader • XML / JSON Reader • JDBC / JPA Reader • ビジネスロジック の実装 • Readerと同様に ライブラリ提供あり
  22. 22. ステップ: バッチレット方式 • ステップ毎に1回だけ呼び出す方式 • ファイル転送、ファイル圧縮、ジョブ完了メール通知 • Spring Batch では Tasklet と呼ばれている Step BatchletjBatch Runtime process() ステップの終了コード ファイル転送、 ファイル圧縮処理など
  23. 23. ステップ以外のジョブ構成要素 #1 • Flow: ステップをグループ化する • Decision: 条件分岐 • Split: 並列実行。Flow毎に可能 (Step毎は不可) ジョブ Step1-1 Step2-1 Step1-2 Flow2 Flow3 Flow1 Decision 終了 Split
  24. 24. ステップ以外のジョブ構成要素 #2 • Listener: 各ジョブ要素の前後に呼ばれる • インターセプタのようなイメージ • 特にスキップ時のSkipListenerはよく使う (ロギング等) ジョブ Step1 Job Listener Item Processor Item Writer Item Reader Step Listener Skip Listener Retry Listener
  25. 25. ここまでのまとめ • jBatchはバッチフレームワークの標準仕様 • アーキテクチャとジョブXML、APIを規定 • XMLで順序定義し、ステップをAPIに沿って実装 • ジョブがトップレベル。1つ以上のステップを含 む • ステップは Chunk方式 or Batchlet方式 で実装 • Chunk方式ではReader/Processor/Writerを実装
  26. 26. 本日のコンテンツ • jBatchとは何か • jBatchをどうやって使うのか • メリット と デメリット • よくある疑問 • まとめ
  27. 27. jBatch利用の流れ 1. ジョブ設計 • ジョブとステップを抽出する 2. ジョブ実装 • ジョブXMLの作成 • ステップの実装 (Reader/Processor/Writer) • ジョブの起動
  28. 28. ジョブ設計 • ジョブをステップに分割 • 各ステップの入力と出力を決める 請求書発行ジョブ 【Step1】 請求額確定 【Step2】 請求書生成 【Step3】 メール送付 CSV aa,bb DBMS 入力 出力 PDF 入力 出力 請求先へ 他システムから 受領した売上データ 請求額TBL 請求書PDF
  29. 29. ジョブ実装 #1 ジョブXMLの作成 <job id=“issueBill”> <!-- Step1 請求額確定 --> <step id=“importInvoice” next=“genPDFBill” > <chunk item-count=“100”> <reader ref=“safesFileReader”/> <processor ref=“billProcessor”/> <writer ref=“billWriter”/> </chunk> </step> <step id=“genPDFBill”> ... チャンク方式でファイルを読み、DBに書くステップ定義 META-INF/batch-jobs/issueBill.xml 次ステップは請求書生成 読込⇒処理 x 100回後に 出力する(デフォルト10) ItemReader, ItemProcessor, ItemWriter定義 クラス名の頭を小文字 or FQCN で指定
  30. 30. ジョブ実装 #1-1 ジョブプロパティ定義 <job id=“issueBill”> <!-- Step1 請求額確定 --> <step id=“importInvoice” next=“genPDFBill” > <chunk item-count=“100”> <reader ref=“safesFileReader”> <properties> <property name=“fixedFileName” value=“input.csv”/> <property name=“fileName” value=“#{jobParameters[‘fileName’]}”/> </properties> </reader> <processor ref=“billProcessor”/> <writer ref=“billWriter”/> </chunk> </step> ... ItemReaderの入力ファイル名を定義する。 固定の場合と、可変の場合で定義方法は異なる。 固定的なプロパティは 値を直接ジョブXMLに定義する 可変プロパティはプロパティ名を定義 後述のジョブ起動引数で値を指定する
  31. 31. ジョブ実装 #1-2 スキップ定義を追加 <job id=“issueBill”> <!-- Step1 請求額確定 --> <step id=“importInvoice” next=“genPDFBill” > <chunk item-count=“100” skip-limit=“10”> <reader ref=“safesFileReader”> <properties> <property name=“fixedFileName” value=“input.csv”/> <property name=“fileName” value=“#{jobParameters[‘fileName’]}”/> </properties> </reader> <processor ref=“billProcessor”/> <writer ref=“billWriter”/> <skippable-exception-class> <include class=“sample.InvalidRecordException”/> </skippable-exception-class> </chunk> <listeners> <listener ref=“logErrorRecordListener” /> <listeners> </step> ... エラー行をスキップしたい場合は、さらに設定追加 10行までスキップする それ以上はジョブ異常終了 スキップ対象例外 (FQCN) スキップ時リスナの定義
  32. 32. ジョブ実装 #2 ステップの実装 • Chunkを構成する各クラスを実装する • スキップリスナを実装する 【Step1】 請求額確定 CSV aa,bb DBMS 入力 出力 請求額TBL Bill Processor SalesFile Reader Bill Writer LogErrorRecordListener 【Step2】 請求書生成 【Step3】 メール送付
  33. 33. ジョブ実装 #2-1 ItemReaderの実装 @Named public class SalesFileReader implements ItemReader { public void open(Serializable checkpoint) { } public Object readItem() throws Exception{ } public void close() { } public Serializable checkpointInfo() { } } ItemReaderには4つのAPIが定義されている
  34. 34. ジョブ実装 #2-1-1 ItemReader @BatchPropertyによるパラメータ取得 @Named public class SalesFileReader implements ItemReader { @Inject @BatchProperty(name=“fileName”) private String fileName; ... ジョブXMLのプロパティ名を @BatchProperty引数に指定 <property name=“fileName” value=“#{jobParameters[‘fileName’]}”/>
  35. 35. ジョブ実装 #2-1-2 ItemReader.Open @Named public class SalesFileReader implements ItemReader { ... private BufferedReader reader; private int rowNum; public void open(Serializable checkpoint) { reader = Files.newBufferedReader(Paths.get(fileName)); if (checkpoint != null) { // TODO skip reader } } public Serializable checkpointInfo() { return rowNum; }
  36. 36. ジョブ実装 #2-1-2 ItemReader.Open @Named public class SalesFileReader implements ItemReader { ... private BufferedReader reader; private int rowNum; public void open(Serializable checkpoint) { reader = Files.newBufferedReader(Paths.get(fileName)); if (checkpoint != null) { // TODO skip reader } } public Serializable checkpointInfo() { return rowNum; } チェックポイント毎に呼ばれる (item-count属性に設定した間隔) 初回スタート時はnull。 リスタート時には異常終了前最後 のcheckpointInfo()の値が渡される
  37. 37. ジョブ実装 #2-1-3 ItemReader.readItem @Named public class SalesFileReader implements ItemReader { ... public Object readItem() { String line = reader.readLine(); if (line == null) return null; // TODO1 line to Sales obj // TODO2 入力値チェック if (!isValid()) { throw new InvalidRecordException(rowNum, line); } rowNum++; return sales; } 入力値チェックもItemReaderの役 割。 エラー時はスキップ対象例外を返 す。 入力がなくなったらnullを返 す
  38. 38. ジョブ実装 #2-1-4 ItemReader.close @Named public class SalesFileReader implements ItemReader { ... public void open(Serializable checkpoint) { // 済 } public Serializable checkpointInfo() { //済 } public Object readItem() throws Exception { // 済 } public void close() { reader.close(); } ... リソースクローズ処理。 readItem()から null が返される or 異常終了時に呼ばれる。
  39. 39. AbstractItemReaderにより省略可 @Named public class SalesFileReader extends AbstractItemReader { @Override public Object readItem() throws Exception { ... } } 一部メソッドは実装不要時に使用。readItem()のみ必須。
  40. 40. ジョブ実装 #2-2 ItemProcessorの実装 • ItemReaderで読んだ値を引数に処理 • ビジネスロジック実装部分 【Step1】 請求額確定 CSV aa,bb DBMS 入力 出力 請求額TBL Bill Processor SalesFile Reader Bill Writer LogErrorRecordListener 【Step2】 請求書生成 【Step3】 メール送付 済
  41. 41. ジョブ実装 #2-2-1 ItemProcessorの実装 @Named public class BillProcessor implements ItemProcessor { @Override public Object processItem(Object item) throws Exception { Sales sales = (Sales) item; // ビジネスロジックをここに実装 // TODO 売上データ ⇒ 請求額 算出 return bill; } } ビジネスロジックをprocessItem()に実装する
  42. 42. ジョブ実装 #2-3 ItemWriterの実装 • ItemProcessorで処理した値を引数に • データを書き出す部分 【Step1】 請求額確定 CSV aa,bb DBMS 入力 出力 請求額TBL Bill Processor SalesFile Reader Bill Writer LogErrorRecordListener 【Step2】 請求書生成 【Step3】 メール送付 済 済
  43. 43. ジョブ実装 #2-3-1 ItemWriterの実装 @Named public class BillWriter implements ItemWriter { public void open(Serializable checkpoint) { } public void writeItems(List<Object> items) throws Exception { } public void close() { } public Serializable checkpointInfo() { } } ItemReaderとの違いはwriteItemsのみ
  44. 44. ジョブ実装 #2-3-1 ItemWriterの実装 @Named public class BillWriter implements ItemWriter { @Override public void writeItems(List<Object> items) throws Exception { List<Bill> bills = (List<Bill>) items; bills.stream() .forEach(this::insertToDB); } private void insertToDB(Bill bill) { .. } ... } 複数のRead => Process 結果のリストが引数に設定 コミットはwriteItemメソッドの呼び出し毎に実行
  45. 45. ジョブ実装 #2-4 スキップリスナ • スキップ対象例外の発生時に呼ばれる • エラーレコードをロギングしたい 【Step1】 請求額確定 CSV aa,bb DBMS 入力 出力 請求額TBL Bill Processor SalesFile Reader BillAmount Writer LogErrorRecordListener 【Step2】 請求書生成 【Step3】 メール送付 済 済 済
  46. 46. ジョブ実装 #2-4-1 スキップリスナの実装 @Named public class LogErrorRecordListener implements SkipReadListener { private static final Logger log = ...; @Override public void onSkipReadItem(Exception e) { InvalidRecordException ex = (InvalidRecordException) e; log.warn(“Row ”+ ex.getNum() + “ is Invalid Record.”); } } スキップした行情報のロギング。 スキップした行全体を別ファイルに書出すのもあり。
  47. 47. ジョブ実装 #3 ジョブの起動 • JobOperator経由で起動する • EJBタイマ、JAX-RS/Servletなど で起動 【Step1】 請求額確定 Bill Processor SalesFile Reader BillAmount Writer LoggingErrorRecordListener 【Step2】 請求書生成 【Step3】 メール送付 請求書発行ジョブ 済 済済 済 JobOperator EJBタイマ JAX-RS/Servlet
  48. 48. ジョブ実装 #3-1 ジョブの起動 @ScheduleによるEJB タイマー @javax.ejb.Singleton public class ScheduleTimer { @Schedule(hour=“4”, minute=“2”) public void timeout() { // start JobOperator jobOperator = BatchRuntime.getJobOperator(); Property props = new Property(); props.setProperty(“fileName”, “/var/xxx/input.csv”); jobOperator.start(“issueBill”, props); } } ジョブIDと、ジョブプロパティを引数に起 動
  49. 49. ジョブ実装 #3-2 ジョブの起動 EEサーバ上なのでJAX-RS経由でも起動できる @Path(“/jobs/{jobId}”) public class BatchResource { @POST public void start(@PathParam String jobId) { // start JobOperator jobOperator = BatchRuntime.getJobOperator(); Property props = new Property(); props.setProperty(“fileName”, “/var/xxx/input.csv”); jobOperator.start(“issueBill”, props); } ...
  50. 50. ジョブ実装 #3-3 ジョブの停止 startの返り値のジョブ実行IDを引数に停止 @Path(“/jobs/{jobId}”) public class BatchResource { @POST public void start(@PathParam String jobId) { // start JobOperator jobOperator = BatchRuntime.getJobOperator(); long execId = jobOperator.start(jobId, props); // stop jobOperator.stop(execId); } ...
  51. 51. ジョブ実装 #3-4 ジョブの再実行 Reader/Writerのopen引数にチェックポイントが設定 @Path(“/jobs/{jobId}”) public class BatchResource { @POST public void start(@PathParam String jobId) { // start JobOperator jobOperator = BatchRuntime.getJobOperator(); long execId = jobOperator.start(jobId, props); // restart jobOperator.restart(execId, props); } ...
  52. 52. まとめ : jBatch利用の流れ 1. ジョブ設計 • ジョブを抽出。ステップと入出力を定義する。 2. ジョブ実装 • ステップの流れ、プロパティをジョブXMLに定義 • ItemReader/ItemProcessor/ItemWriterのJava実装 • JobOperator経由で起動、停止、リスタート制御 • EJBタイマ、JAX-RS Web-API 等 から起動
  53. 53. 本日のコンテンツ • jBatchとは何か • jBatchをどうやって使うのか • メリット と デメリット • よくある疑問 • まとめ
  54. 54. jBatch適用のメリット • 一定の型に填めて開発できる • 1つ1つのジョブ構造のAP設計簡略化 • 第3者(保守・改造時) が構造を理解しやすい • チェックポイント等のバッチ共通機能の恩恵 • Java EE サーバの機能が使える • JAX-RS(REST入出力) / JMS連携 / CDIによるDI • リソース管理: DB接続プール、スレッドプール
  55. 55. jBatch適用のデメリット • 型に填めにくい処理への適用が難しい • 複数入力(マスタ / トランザクション読込)、複数出力 • 読み込み方の工夫で解決できることもある CSV aa,bb 商品マスタ 【Step】 売上データ集計 ProcessorReader Writer CSV aa,bb 売上データ public class Aggregate { private Master master; private List<Transaction> trans; 1マスタ/複数トランザクションデータ毎 に読み込み DBMS 集計済 売り上げ
  56. 56. jBatch適用のデメリット • 処理によっては記述が冗長 • XMLによる処理の流れの定義 • java.nio.file.Files.linesメソッドで十分な場合もある // CSVファイルより、15歳以上のみ抽出して別ファイルへ try (Stream<String> stream = Files.lines(Paths.get(“csv.txt”)) { stream .map(line -> line.split(“,”)) .map(arr -> // TODO array to Student) .filter(student -> student.getAge() > 15) .forEachOrdered(s -> // write to file); } Reader Processor Writer
  57. 57. 本日のコンテンツ • jBatchとは何か • jBatchをどうやって使うのか • メリット と デメリット • よくある疑問 • まとめ
  58. 58. Question #1 Spring Batch と何が違うのか?
  59. 59. jBatch と SpringBatch の違い #1 • jBatchは仕様、SpringBatchは実装 • jBatch実装: RI (GlassFish), JBeret (WildFly) • SpringBatch3.0より jBatchに準拠 • ライブラリの豊富さは先行のSpringBatch有利 SpringBatchの機能範囲 jBatch アーキテクチャ ジョブXML API定義 • 豊富なライブラリ群 • CSV/TSV, XML, JDBCページング • かゆいところに手が届く • 同一形式の複数ファイル読込 (stock_1.csv, stock_2.csv ...) • 複数行を1アイテムにマッピング
  60. 60. jBatch と SpringBatch の違い #2 • SpringBatchはジェネリクス対応 public class Reader implements ItemReader<Stock> { @Override public Stock read() { // read stock } } public class Processor implements ItemProcessor<Stock, Result> { @Override public Result process(Stock s) throws Exception { ... } } ジェネリクス対応により キャストが不要
  61. 61. Question #2 もうXMLはつらいです。
  62. 62. Spring Batchでは Java でジョブ定義可能 @Bean public Job job() { return jobsBuilderFactory.get("myJob") .start(step1()).next(step2()) .build(); } @Bean protected Step step1(...) { return stepBuilderFactory.get("step1") .<Person, Person> chunk(10) .reader(reader).processor(processor).writer(writer) .build(); } Spring Batch - Reference Documentation http://docs.spring.io/spring-batch/trunk/reference/html/configureJob.html#javaConfig
  63. 63. NetBeans “jBatch Suite” • NetBeanプラグインによる jBatch サポート • ジョブXMLの生成 • ブランク ItemXXX/Batchlet 生成 ドラック & ドロップ で ステップの流れを作成
  64. 64. Question #3 ジョブの流れ定義は ジョブスケジューラ or jBatch ?
  65. 65. やりたいことに応じて使い分ける • システム全体のジョブ管理にはスケジューラ • APサーバだけで完結しないジョブへの対応 • 例: ログバックアップ、サーバ再起動 • ジョブAP内の順序制御には jBatch • Javaコード (Decision) による複雑な条件判定も可 • job.xml変更時は *.war 再デプロイが必要
  66. 66. jBatch実装のこれからに期待 GlassFish4はテーブル形式でジョブ履歴を表示 • 管理コンソールが強化されると jBatch はより便利に • ジョブフロー状況の図示、ジョブ間の順序制御
  67. 67. Question #4 スケールアウトは可能か?
  68. 68. 今のところ仕様上は未対応 • jBatchの仕様としては定義されていない • 同一ジョブを複数サーバにデプロイする等の対処 • 実装サーバでの対応に期待 • クラスタ機能と連携したジョブ分散 • ジョブ毎のリソース管理 (スレッド数、Javaヒープ等)
  69. 69. 本日のコンテンツ • jBatchとは何か • jBatchをどうやって使うのか • メリット と デメリット • よくある疑問 • まとめ
  70. 70. まとめ #1 JobOperator Job Item Processor Item Writer <job id=“monthly_billing”> <step id=“step1” next=“step2”> <step id=“step2”> </job> Step1 Step2 Step3 Job Repository Item Reader public interface ItemReader { Object readItem() throws ... } jBatchはジョブを実装する為のフレームワーク。
  71. 71. まとめ #2 jBatchの利用により実現できること。 • ジョブXMLによる順序制御 • APの構造を決める (Reader/Processor/Writer) • チェックポイント管理 • エラーハンドリング (スキップ/リトライ) • ジョブ実装で Java EE の各種機能が使える
  72. 72. まとめ #3 【ここが嬉しい】 • Java EE でバッチ処理が注目され始めた • 一定の型に填めてジョブ設計を再利用できる • ジョブに必要な共有機能が享受できる 【もう少しな部分】 • 型に填めにくい / 塡まらない場合もある • 現状は先発のSpring Batchの方が多機能
  73. 73. 最後に 既にjBatch対応製品がリリースされています。 OSS製品なので是非まずは試してみてください! OracleとJavaは、Oracle Corporation及びその子会社、関連会社の米国及びその他の国における登録商標です。 文中の社名、商品名等は各社の商標または登録商標である場合があります。 GlassFish4.1

×