20211123のJavaに関する記事は10件です。

決定版〜継承vsインタフェースのベストプラクティス

こんにちは。歴13年のアプリエンジニアです(iOS/Android、Best of AppStore?受賞歴あり)。 ふだんは自分が手がけているスタートアップと数社の技術顧問をしているのですが、長年さまざまなプロジェクトで目の当たりにしてきたアプリ(ソフトウエア)開発上の問題について、設計とプログラミング双方の観点からのベストプラティクスを、ルーディメンツ(Rudiments)と名づけシンプルで理解しやすい形で提唱を目指し準備中しています。 今日はこの中から最もポピュラーな問題である、「継承vsインタフェース」(Interface as inheritance alternative)を取り上げます。特に初級者の方にとってより理解が深まれば幸いです。 Interface as inheritance alternative あるクラスを新しく作り、既存のクラスと共通の型にしたい場合、クラスの継承を使うべきか、interface(Swiftではprotocol)を使うべきかは多くのエンジニアにとって常につきまとう問題である。 採用基準 筆者は継承とinterfaceの採用基準として以下の基準を推奨している。 基準①:既存のクラスと機能や役割的に大きな部分が共通しているとみなせる場合は継承、そうでなく機能を超えて部分的に共通な役割を設定したい場合にはinterface 基準②:多くの言語では多重継承ができないため、将来のアップデートでさらなる拡張が見込まれる場合(柔軟性を確保したい場合)にはinterface、クラス同士の依存関係を固定させ堅牢性を確保したい場合には継承 基準③:「何を(具体性)」が重要な場合は継承、「どんな(抽象性)」が重要な場合はinterface 基準④:クラスの分布のイメージが縦断的/階層的/中央集権的な場合は継承、横断的/網羅的/分散的な場合はinterface ※筆者はよくポケモンの例を挙げて説明している。すなわち、あるポケモンが進化して新しいポケモンになる場合は継承、属性(水属性や火属性など)はinterfaceを採用するのが望ましい。上の4つの基準に照らして妥当かどうかを考えてみてほしい。 ルーディメンツ ここでは継承の代替としてのinterface(protocol)による実装をルーディメンツとして挙げる。interface(protocol)は特にデータモデル系のクラスに対して相性が良い。 ※サンプルコードはswiftで書かれています。今後他の言語も追加予定 protocol Feedable { var feedIndex: Int { get set } var feedColor: UIColor { get } } protocol Locatable: class { var location: CLLocationCoordinate2D { get set } var mapMeters: Int { get } } class User: Feedable { // MARK: Feedable var feedIndex: Int = 0 var feedColor: UIColor { // readonly return .black } } class Shop: Feedable, Locatable { var latitude: Double = 0 var longitude: Double = 0 // MARK: Feedable var feedIndex: Int = 0 var feedColor: UIColor { // readonly return .red } // MARK: Locatable var location: CLLocationCoordinate2D { get { return CLLocationCoordinate2DMake(latitude, longitude) } set { latitude = newValue.latitude longitude = newValue.longitude } } var mapMeters: Int { return 1000 } } // Caller側ではクラスに依存せずに処理が可能になる var feedArray: [Feedable] = [User(), Shop()] feedArray.enumerated().forEach { index, item in item.feedIndex = index // 個別の処理を行いたければswitchで行える switch item { case let item as User: // do something case let item as Shop: // do something } } ? アンチパターン① 本ルーディメンツを継承で実装するとこうなる。 class BaseItem { var feedIndex: Int = 0 var feedColor: UIColor { return .clear } var location: CLLocationCoordinate2D = CLLocationCoordinate2DMake(0.1, 0.1) var mapMeters: Int { return 0 } } class User: BaseItem { // ... } class Shop: BaseItem { // ... } この実装は設計上無理がある。先述の4つの基準に当てはめて考える。 基準①:UserとShopは機能や役割的に共通点が多いとは言えないので継承にそぐわない 基準②:今後他のクラスが出てきたときにBaseItemを拡張しなくてはならず、柔軟性を損なうため継承にはそぐわない 基準③:BaseItemはクラス名のとおり具体性がなく無理矢理感がある。よって継承にはそぐわない 基準④:UserとShopの関係は階層関係とはいえず、むしろ横断的関係のほうが適合する。よって継承にはそぐわない ? アンチパターン② 入門書などで継承の説明で以下のようなコード例をよく見かけるが、これは望ましい実装ではなく、むしろアンチパターンである。 class Animal { func bark() {} } class Dog: Animal { override func bark() { print("わんわん?") } } class Cat: Animal { override func bark() { print("ニャーニャー?") } } もし今後仕様追加でRobotというクラスを追加しなくてはならなくなった場合、基底クラスがAnimalでは意味が通じずコードの保守性が低下してしまう。またRobotは鳴かない(bark()関数が必要ない)。 そこでinterfaceを使って次のように実装すべきである。 protocol Barkable { func bark() } class Dog: Barkable { func bark() { print("わんわん?") } } class Cat: Barkable { func bark() { print("ニャーニャー?") } } これだと新しいクラスRobotを追加してもBarkableを実装しなければ済むだけである。 継承とは異なり、大多数のプログラミング言語ではインタフェースの多重実装を認めている。新しい型が欲しければ新しいインタフェースを作成して、それを追加実装すれば良い。 おわりに いかがだったでしょうか。ぜひ閲覧者の皆さんの意見や批判、感想をいただけましたら幸いです。 序文にある通り、筆者は現在これまでの経験を踏まえた新しいソフトウエア開発のトレーニングメソッド(The Programming Rudiments)の策定を目指しています(現在準備段階で17パターンを用意中)。 ソフトウエア(アプリ)開発においては、現在でも入門書と現場レベルの間には大きな乖離があり、それを埋めるトレーニングメソッドに目ぼしいものがないという状況が続いています(たとえば、有名どころのデザインパターンは抽象度が高く難解で初級者には実感が湧きづらいといった問題があります)。 ぜひ皆さんのご意見、ご感想をお待ちしています・・・!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【決定版】継承vsインタフェースのベストプラクティス

こんにちは。歴13年のアプリエンジニアです(iOS/Android、Best of AppStore?受賞歴あり)。 ふだんは自分が手がけているスタートアップと数社の技術顧問をしているのですが、長年さまざまなプロジェクトで目の当たりにしてきたアプリ(ソフトウエア)開発上の問題について、設計とプログラミング双方の観点からのベストプラティクスを、ルーディメンツ(Rudiments)と名づけシンプルで理解しやすい形で提唱を目指し準備しています。 今日はこの中から最もポピュラーな問題である、「継承vsインタフェース」(Interface as inheritance alternative)を取り上げます。特に初級者の方にとってより理解が深まれば幸いです。 Interface as inheritance alternative あるクラスを新しく作り、既存のクラスと共通の型にしたい場合、クラスの継承を使うべきか、interface(Swiftではprotocol)を使うべきかは多くのエンジニアにとって常につきまとう問題である。 採用基準 筆者は継承とinterfaceの採用基準として以下の基準を推奨している。 基準①:既存のクラスと機能や役割的に大きな部分が共通しているとみなせる場合は継承、そうでなく機能を超えて部分的に共通な役割を設定したい場合にはinterface 基準②:多くの言語では多重継承ができないため、将来のアップデートでさらなる拡張が見込まれる場合(柔軟性を確保したい場合)にはinterface、クラス同士の依存関係を固定させ堅牢性を確保したい場合には継承 基準③:「何を(具体性)」が重要な場合は継承、「どんな(抽象性)」が重要な場合はinterface 基準④:クラスの分布のイメージが縦断的/階層的/中央集権的な場合は継承、横断的/網羅的/分散的な場合はinterface ※筆者はよくポケモンの例を挙げて説明している。すなわち、あるポケモンが進化して新しいポケモンになる場合は継承、属性(水属性や火属性など)はinterfaceを採用するのが望ましい。上の4つの基準に照らして妥当かどうかを考えてみてほしい。 ルーディメンツ ここでは継承の代替としてのinterface(protocol)による実装をルーディメンツとして挙げる。interface(protocol)は特にデータモデル系のクラスに対して相性が良い。 ※サンプルコードはswiftで書かれています。今後他の言語も追加予定 protocol Feedable { var feedIndex: Int { get set } var feedColor: UIColor { get } } protocol Locatable: class { var location: CLLocationCoordinate2D { get set } var mapMeters: Int { get } } class User: Feedable { // MARK: Feedable var feedIndex: Int = 0 var feedColor: UIColor { // readonly return .black } } class Shop: Feedable, Locatable { var latitude: Double = 0 var longitude: Double = 0 // MARK: Feedable var feedIndex: Int = 0 var feedColor: UIColor { // readonly return .red } // MARK: Locatable var location: CLLocationCoordinate2D { get { return CLLocationCoordinate2DMake(latitude, longitude) } set { latitude = newValue.latitude longitude = newValue.longitude } } var mapMeters: Int { return 1000 } } // Caller側ではクラスに依存せずに処理が可能になる var feedArray: [Feedable] = [User(), Shop()] feedArray.enumerated().forEach { index, item in item.feedIndex = index // 個別の処理を行いたければswitchで行える switch item { case let item as User: // do something case let item as Shop: // do something } } ? アンチパターン① 本ルーディメンツを継承で実装するとこうなる。 class BaseItem { var feedIndex: Int = 0 var feedColor: UIColor { return .clear } var location: CLLocationCoordinate2D = CLLocationCoordinate2DMake(0.1, 0.1) var mapMeters: Int { return 0 } } class User: BaseItem { // ... } class Shop: BaseItem { // ... } この実装は設計上無理がある。先述の4つの基準に当てはめて考える。 基準①:UserとShopは機能や役割的に共通点が多いとは言えないので継承にそぐわない 基準②:今後他のクラスが出てきたときにBaseItemを拡張しなくてはならず、柔軟性を損なうため継承にはそぐわない 基準③:BaseItemはクラス名のとおり具体性がなく無理矢理感がある。よって継承にはそぐわない 基準④:UserとShopの関係は階層関係とはいえず、むしろ横断的関係のほうが適合する。よって継承にはそぐわない ? アンチパターン② 入門書などで継承の説明で以下のようなコード例をよく見かけるが、これは望ましい実装ではなく、むしろアンチパターンである。 class Animal { func bark() {} } class Dog: Animal { override func bark() { print("わんわん?") } } class Cat: Animal { override func bark() { print("ニャーニャー?") } } もし今後仕様追加でRobotというクラスを追加しなくてはならなくなった場合、基底クラスがAnimalでは意味が通じずコードの保守性が低下してしまう。またRobotは鳴かない(bark()関数が必要ない)。 そこでinterfaceを使って次のように実装すべきである。 protocol Barkable { func bark() } class Dog: Barkable { func bark() { print("わんわん?") } } class Cat: Barkable { func bark() { print("ニャーニャー?") } } これだと新しいクラスRobotを追加してもBarkableを実装しなければ済むだけである。 継承とは異なり、大多数のプログラミング言語ではインタフェースの多重実装を認めている。新しい型が欲しければ新しいインタフェースを作成して、それを追加実装すれば良い。 おわりに いかがだったでしょうか。ぜひ閲覧者の皆さんの意見や批判、感想をいただけましたら幸いです。 序文にある通り、筆者は現在これまでの経験を踏まえた新しいソフトウエア開発のトレーニングメソッド(The Programming Rudiments)の策定を目指しています(現在準備段階で17パターンを用意中)。 ソフトウエア(アプリ)開発においては、現在でも入門書と現場レベルの間には大きな乖離があり、それを埋めるトレーニングメソッドに目ぼしいものがないという状況が続いています(たとえば、有名どころのデザインパターンは抽象度が高く難解で初級者には実感が湧きづらいといった問題があります)。 ぜひ皆さんのご意見、ご感想をお待ちしています・・・!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく忘れるチートシート(Java編)

Javaでよく忘れるやつの覚え書き 同確はPCにJavaを入れるか https://paiza.io/ja この辺で エラーが出たら申し訳bangatです プログラミング5大要素 1. 変数 Integer IntegerSample.java import java.io.PrintStream; import java.lang.Integer; class IntgerSample { public static void main(String[] args) throws Exception { Integer num = 10; System.out.println(num); } } String StringSample.java import java.io.PrintStream; import java.lang.String; class StringSample { public static void main(String[] args) throws Exception { String str = "abcdefg"; System.out.println(str); } } 2. 配列 Array:JavaはArrayよりListで扱う方がいいよ(Arrayだと中身が見えないからデバッグしづらい) ArraySample.java import java.io.PrintStream; import java.lang.Integer; import java.lang.reflect.Array; class ArraySample { public static void main(String[] args) throws Exception { Integer[] numArray = { 1, 2, 3, 4, 5 }; System.out.println(numArray); } } List:Listはインターフェイスなので実際に使用する際は ArrayList, LinkedList, Arrays.asList() で作成したインスタンスを代入します ListSample.java import java.io.PrintStream; import java.lang.Integer; import java.util.List; import java.util.Arrays; class ListSample { public static void main(String[] args) throws Exception { List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5); System.out.println(numList); } } ArrayList:可変長の配列 ArrayListSample.java import java.io.PrintStream; import java.lang.Integer; import java.util.List; import java.util.ArrayList; class ArrayListSample { public static void main(String[] args) throws Exception { List<Integer> numList = new ArrayList<>(); numList.add(1); numList.add(2); numList.add(3); System.out.println(numList); } } Map:連想(Hash)配列 MapもListと同じインターフェイスなので実際に使うのは HashMap等を使います MapSample.java import java.io.PrintStream; import java.lang.Integer; import java.util.HashMap; import java.util.Map; class MapSample { public static void main(String[] args) throws Exception { Map<String, Integer> alfabetHash = new HashMap<>() { { put("a", 1); put("b", 2); } }; System.out.println(alfabetHash); System.out.println(alfabetHash.get("a")); System.out.println(alfabetHash.get("b")); } } 3. ループ Foreach LoopSample.java import java.io.PrintStream; import java.lang.Integer; import java.util.List; import java.util.Arrays; import java.util.Map; import java.util.HashMap; class LoopSample { public static void main(String[] args) throws Exception { List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5); Map<String, Integer> alfabetHash = new HashMap<>(){ { put("a", 1); put("b", 2); put("c", 3); } }; // 単一行で書く場合 numList.forEach(num -> System.out.println(num)); System.out.println("--------------"); // 複数行で書く場合 numList.forEach(num -> { num *= 10; System.out.println(num); }); System.out.println("--------------"); // 引数が複数の場合 alfabetHash.forEach((key, value) -> { System.out.printf("key=%s, value=%s%n", key, value); }); } } 多重ループ NestLoopSample.java import java.io.PrintStream; import java.lang.Integer; import java.lang.String; import java.util.List; import java.util.Map; import java.util.ArrayList; import java.util.HashMap; class NestLoopSample { public static void main(String[] args) throws Exception { List<Map<String, Integer>> listHash = new ArrayList(); listHash.add(new HashMap<>() { { put("a", 1); put("b", 2); } }); listHash.add(new HashMap<>() { { put("a", 10); put("b", 20); } }); listHash.add(new HashMap<>() { { put("a", 100); put("b", 200); } }); System.out.println(listHash); System.out.println("---------------"); listHash.forEach(rowHash -> { System.out.println(rowHash); System.out.printf("a: %d", rowHash.get("a")); System.out.println(""); System.out.printf("b: %d", rowHash.get("b")); System.out.println(""); }); } } 4. 条件分岐 IfSample.java import java.io.PrintStream; import java.lang.Integer; import java.lang.String; class IfSample { public static void main(String[] args) throws Exception { Integer a = 99; System.out.printf("aの値は %d", a); System.out.println(""); if (a < 100) { System.out.println("aは100未満です"); } a++; System.out.printf("インクリメント後のaの値は %d", a); System.out.println(""); if (a == 100) { System.out.println("aは100です"); } if (a <= 100) { System.out.println("aは100以下です"); } a++; System.out.printf("インクリメント後のaの値は %d", a); System.out.println(""); if (a > 100) { System.out.println("aは100より大きいです"); } if (a >= 100) { System.out.println("aは100以上です"); } if (a == 500) { // なんか処理 } else { System.out.printf("いいえ、500ではありません aは%dです", a); System.out.println(""); } if (a != 500) { System.out.println("はい、aは500ではありません"); } } } 5. メソッド Main.java import java.io.PrintStream; import java.lang.String; class Main { public static void main(String[] args) throws Exception { String message = new MethodSample().getMessage(); System.out.println(message); } } class MethodSample { MethodSample() { } public String getMessage() { return "hallow. selamat pagi."; } } 6. コンパイル関連 // export JAVA_HOMEはしておくこと $ javac -d classes classes Sample.java // jarファイルの中を見る $ jar tf JarSample.jar
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

逆引きMicronaut

MicronautフレームワークでのRestful API実装を試してみた際に調べたことを整理した。(作成したプロジェクトはmicronaut-rest-apiに置いている。) 公式ガイドライン・リファレンス Micronaut本体:User Guide API Reference 周辺ライブラリ(Micronaut Dataなど):User Guide一覧 開発環境 (InteliJ) セットアップ・プロジェクト作成 Micronaut CLIをインストールする。Windows環境にはChocolateyでインストールできる。 mnコマンドでプロジェクトを新規作成する。 mn create-app --build=gradle_kotlin --lang=kotlin --jdk=11 --test=junit --features=jax-rs io.github.unhurried.micronaut-rest-api mnコマンドを使う方法の他にもMicronaut Launchでプロジェクトを作成してダウンロードすることもできる。(参考:Kotlinベースのプロジェクト設定) InteliJ IDEAにインポートしてアノテーションプロセッサを有効化する。(参考:2.3 Setting up an IDE) 開発用にアプリケーションを(デバッグ)実行する View -> Tool Windows -> Gradle -> applicaition / run から(デバッグ)実行すればよい。 Dependency Injection Micronautはコンパイル時のデータを利用して、リフレクションやプロキシにできる限り頼らない方法でDIを実装しているとのこと。起動時間短縮やメモリ使用量削減のメリットがある。(参考:3 Inversion of Control) JSR-330 (javax.inject) の実装となっているため@Namedや@Injectを使った設定が可能な上に、他のDIコンテナ同様に独自のスコープ定義も用意されている。(参考:3.7.1 Built-In Scopes) Restful APIを実装する MicronautではHTTPサーバーとして動かすための独自のAPIが提供されている。HTTPサーバーにはNettyが使われている。(参考:6 The HTTP Server) 他のフレームワークと同様に、@Controllerや@Getなどのアノテーションを使ってコントローラクラスを実装できる。 エラー処理は例外もしくはステータスコードに対してエラーハンドラを定義する。エラーハンドラはコントローラごと(ローカルエラーハンドラ)もしくはグローバルの2種類がある。(参考:6.17 Error Handling) Restful APIのベースパスを設定するには、micronaut.server.context-pathを設定すればよい。(参考:Configuration Reference) Micronaut JAX-RSを使うとJAX-RS APIもある程度使える。 Micronaut JAX-RSはJAX-RS実装ではなく、あくまでよく使われるAPIをMicronautで使えるようにすることが目的とのことで、全てのAPIがサポートされている訳ではない。例えば、JAX-RSアノテーションは、実際にはコンパイル時にMicronautのアノテーションに置き換えることで対応されている。 サポートされないAPIと回避策 @BeanParamがサポートされていないため、クエリ・ヘッダパラメータをクラスにまとめることができない。リソースメソッドの引数として宣言する必要がある。 WebApplicationExceptionをレスポンスに変換する処理が実装されていないので、自力でエラーハンドラを用意する必要がある。 Jerseyなどの別のJAX-RS実装をサポートする予定はなさそう。(参考:#4896, #4942) 実装例:ToDoResource.kt, ErrorHandler.kt HTTPリクエスト・レスポンスを操作する HttpRequest, HttpResponseなどの抽象化されたAPIが用意されているためこれらを使えばよい。(参考:6.9 The HttpRequest and HttpResponse) データベースアクセスを実装する JDBC・JPAサポート(Micronaut SQL) MicronautではJDBCもしくはJDBC + JPA (Hibernate) によるデータベースアクセスがサポートされている。(参考:Micronaut 12.2 Cofigurations for Data Access、Micronaut SQL) JDBC(コネクションプール)実装はAapache DBCP2, Hikari, Tomcatの3つの実装から選択できる。 データベースアクセス機能を利用するには、JDBC実装とJDBCドライバの依存関係(JPAを使う場合はHibernateの依存関係も)を設定すればよい。 // Hikari + H2 Databaseの例 runtime("io.micronaut.sql:micronaut-jdbc-hikari") runtime("com.h2database:h2") // JPAを利用する場合は以下も追加する implementation("io.micronaut.sql:micronaut-hibernate-jpa") データソースはapplication.yamlに設定する。(参考:Configuring JDBC Connection Pools) Micronaut Data JDBC・JPAサポートに加えて、Micronaut Dataという関連プロジェクトがあり、GORMやSpring Dataに似たAPIが提供されている。AoTによるリフレクションに頼らない仕組みとのこと。 Micronaut Dataを使うにはJDBC実装とJDBCドライバに加えて以下の依存関係を設定すればよい。 kapt("io.micronaut.data:micronaut-data-processor") // JPAを利用する場合は以下を設定する implementation("io.micronaut.data:micronaut-data-hibernate-jpa:3.1.2") 実装例:ToDoRepository.kt, ToDo.kt トランザクション境界の設定 @Transactional (JTA) もしくはSpring Transactionによるトランザクション境界の設定ができる。(参考:6.6 Transactions) その他の参考資料 ACCESS A DATABASE WITH JPA AND HIBERNATE Bean (DTO, data class)のプロパティをコピーする Apache CommonsやSpring FrameworkのBeanUtilsのように、型の異なるオブジェクト間でプロパティをコピーするユーティリティを実装する。 Micronautではリフレクションに頼らずにBeanのプロパティの読み書きなどの処理を実装するためにBean Introspectionという機能が提供されている。 低レイヤのI/FであるBeanIntrospector/BeanIntrospectionに加えて、抽象化されたBeanWrapper APIも用意されている。 Bean Introspectionを利用するには、以下の依存関係を設定すればよい。 kapt("io.micronaut:micronaut-inject-java") runtimeOnly("io.micronaut:micronaut-core") 実装例:BeanHelper.kt その他の参考資料 https://gitter.im/micronautfw/questions?at=5f19c7fe261ed758ea2321be Aspected Oriented Programmingを実装する Micronautではリフレクションに頼らないcompile-time AOP機能が提供される。(参考:5 Aspect Oriented Programming) コンパイル時にAspectを組み込むクラスのサブクラスを生成する仕組みになっているため、クラスがデフォルトでfinalとなるKotlinではopenを指定する必要がある。これを自動化できるall-openプラグインも提供されているがメソッドには効かないため、メソッドにAOPを設定する場合はopenの指定が必要となる。(参考:14.3.3 Kotlin and AOP Advice) 実装はシンプルで、AOP設定用のアノテーションと、それに対応するAdvice(インターセプタ)クラスを実装すればよい。 ※ AspectJのように適用対象をAdviceクラスに記述するAPIは提供されていない。 実装例:LoggerAdvice.kt, LoggerAspect.kt ログを出力する Slf4jが組み込まれているためLoggerFactoryを使ってクラスに対応するロガーを取得すればよい。(参考:Logging) テストコードを実装する Micronaut Testプロジェクトとしてテスト用の機能が提供されている。Spock, JUnit5, Kotest, Kotlin Testそれぞれの拡張として提供されるため、好きなテストフレームワークを選択できる。 EmbeddedServerインスタンスをDIすることでテスト用にアプリケーションを起動することもできる。 実装例:MicronautRestApiTest.kt
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【俺的】COBOL→Java言語移行ガイド(INDEX)

この記事は『プログラミング学習 Advent Calendar 2021』の20日目のフライング記事です。 この記事について この記事は、年内に書いた「【俺的】COBOL→Java言語移行ガイド」のリンク集です。もうアップもしてしまっていますので、この記事もフライングで公開してしまいます。 このシリーズを書こうとしたきっかけ リンクだけ貼っておしまいは味気ないので、ちょっと自分語りさせてください。 私は大学では情報科に在籍し、SIerに就職したのですが、そこでまさかのまさかでCOBOLに出会いました(学生時代は「COBOLなんてもう動いてないでしょw」とか言ってた矢先でしたから驚きですね)。 そんなこんなで入社して数年立ち、現在もCOBOL業務に従事しています。 しかし転機は訪れます。COBOLからJavaへのマイグレーションの話が飛んできたのです。長期計画のためすぐではないですが、いずれはという話でした。 私は大学でご教授頂いたおかげもあり、Javaは自分でサーブレットやJSPを書いてGlassFish Serverを立ち上げてWeb公開できるくらいにはなっていました(最近数年ぶりにしたら結構忘れていましたが)。それとCOBOLを数年触ってきて思ったのが、「COBOL1本で今までお仕事されてきた方がいきなりJavaとなったとき、大変ではないか」ということです。 そこで、「私のできる範囲でCOBOL→Java(オープン系)マイグレーションとなったとき、ほんの少しでも参考になるページを作れたらいいな」というのが今回の企みでした。 インデックス さて、話を本筋に戻しましょう。 インデックスと書きましたが、ただのリンク集です。 1. 導入 「なぜマイグレーションが進むのか」を独自目線で調べた記事です。 2. 全体構造編 「COBOLとJavaで、全体的な書き方でどう変わってくるのか」を簡単にまとめた記事です。 3. 基本文法編 「COBOLとJavaで、基本文法がどのように変わってくるのか」を簡単にまとめた記事です。 アーカイブ集 私個人の記事になるのですが、過去にCOBOL、Java関係の記事を少し書いて言いますので、何かのご参考になれば幸いです。 Java Javaを使ったオブジェクト指向プログラミング総まとめ Javaのオブジェクト指向プログラミングについて、復習としてまとめた記事です。 COBOL 1人COBOL再研修〜INDEX〜 入社して初めてのCOBOLでしたので、休日に勉強したのをまとめた記事です。この記事は本記事同様インデックス記事です。全5記事あります。 最後に 実際、COBOLとJavaは手続き言語とオブジェクト指向言語な時点で「比べるも何も」というのが正解かもしれませんが、比較して読み替えれる点はいくつかあると思っています。 マイグレーションに従事する方やこれから業務がCOBOL⇔オープンに変わる方に、少しでも支えになれば幸いです。 あと、各記事にLGTMいっぱいください()。 ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【俺的】COBOL→Java言語移行ガイド(基本文法編)

検証環境 COBOL Java cobc (GnuCOBOL) 3.1.2.0 java 14.0.1 2020-04-14 はじめに 前回の記事では、「全体構造から見たCOBOLとJavaの違い」を見ていきました。今回はCOBOLとJavaで、「基本文法がどのように変わってくるのか」を書いていこうと思います。最終回です。 閲覧してほしい方(前編より) これからCOBOLからオープン系へのマイグレーションに従事する方 COBOLとJavaの言語的違いを知っておきたい方 COBOL習熟者で、将来オープン言語を触らなければならないがわからない方 COBOL未経験でマイグレーションされる方や、これからCOBOLエンジニアからJavaエンジニアに移る方の駆け出し資料として参考になれば幸いです。 1.データID ⇔ 変数 Java等オープン言語でいう変数、COBOLでいうデータIDの定義方法は、根本的な考えから違います。 まずそもそも、COBOLでは、好きな箇所でデータIDの定義はできません。使いたいデータIDは、DATA DIVISIONのWORKING-STORAGE SECTIONで定義しなければなりません。 COBOL *--1---------2---------3---------4---------5---------6 DATA DIVISION. WORKING-STORAGE SECTION. 01 VARIABLE PIC X(5). この場合、01 VARIABLE PIC X(5).がデータ名定義で、「レベル01でVARIABLEというデータ名をXタイプ5桁で定義」を意味します。データ名に関して詳しいことについては、こちらをご覧ください。 しかしJavaの場合、変数は使用したいとき、使用したい場所で定義することができます。記法は以下のとおりです。 Java public static void main(String[] args) { // mainメソッドで使用可能なint型variableを定義 int variable; } 2.IF ⇔ if ifにおいては、COBOLとJavaで記法が似ている上、考え方はどちらも同じです。 COBOL *--1---------2---------3---------4---------5---------6 IF CONDITION THEN DISPLAY 'CONDITION IS TRUE.' ELSE DISPLAY 'CONDITION IS FALSE.' END-IF. Java if (condition) { System.out.println("Condition is true."); } else { System.out.println("Condition is false."); } どちらもconditionがtrueのときに、上側のロジックが走ります。 COBOLのほうが、どちらかといえばVB寄りの記法です。 ただし、「かつ」や「または」といった論理演算子については、COBOLとJavaでは違いがあります。 言語 COBOL Java かつ AND && または OR ` 3.EVALUATE ⇔ switch 基本的な使用方法は、COBOLとJavaではあまり変わりません。 COBOL *--1---------2---------3---------4---------5---------6 EVALUATE EXPRESSION WHEN 1 DISPLAY 'EXPRESSION IS 1' WHEN 2 DISPLAY 'EXPRESSION IS 2' WHEN OTHER DISPLAY 'EXPRESSION IS NOT 1 AND 2' END-EVALUATE. Java switch(expression) { case 1: System.out.println("expression is 1"); break; case 2: System.out.println("expression is 2"); break; default: System.out.println("exrepssion is not 1 and 2"); break; } ただし、COBOLでは使用てきていた以下の記法は使用できません。 EXPRESSIONをTRUEとして、WHENに式を書く方法 *--1---------2---------3---------4---------5---------6 EVALUATE TRUE WHEN EXPRESSION = 1 DISPLAY 'EXPRESSION IS 1' WHEN EXPRESSION = 2 DISPLAY 'EXPRESSION IS 2' WHEN OTHER DISPLAY 'EXPRESSION IS NOT 1 AND 2' END-EVALUATE. 2つ以上の項目の同時比較 ANYを使用した判定 *--1---------2---------3---------4---------5---------6 EVALUATE EXPRESSION1 EXPRESSION2 WHEN 1 1 DISPLAY 'EXPRESSION1 IS 1' DISPLAY 'EXPRESSION2 IS 1' WHEN 2 ANY DISPLAY 'EXPRESSION1 IS 2' DISPLAY 'EXPRESSION2 IS ANY' WHEN OTHER *-- EXREPSSION2 ハ ANYデカナラズ イッチスル DISPLAY 'EXPRESSION1 IS NOT 1' END-EVALUATE. 4.PERFORM VARYING UNTIL ⇔ for COBOLとJavaでは、記法が結構変わってきます。 また、COBOLでは終了条件を記載しますが、Javaでは継続条件を記載します。 COBOL *--1---------2---------3---------4---------5---------6 *-- 10カイ LOOP *-- I ハ 1 カラ 1 ズツ フヤシテ I > 10 ニ ナルマデ PERFORM VARYING I FROM 1 BY 1 UNTIL I > 10 DISPLAY 'LOOP:' I END-PERFORM. MAIN-E. STOP RUN. Java // iは1から1ずつi++でインクリメントして、i <= 10の間 for (int i = 1; i <= 10; i++) { System.out.println("Loop:" + i); } 5.PERFORM UNTIL ⇔ while 結局のところ、COBOLでループを使用するときは、PERFORMです。 UNTILのみ残すことで、オープン言語におけるwhileと同じになります。 COBOL *--1---------2---------3---------4---------5---------6 PERFORM UNTIL I > 10 DISPLAY 'LOOP:' I ADD 1 TO I END-PERFORM. Java int i = 1; while (i <= 10) { System.out.println("Loop:" + i); i++; } 最後に ここまでCOBOLとJava(オープン系言語)の比較をしてきました(後半なぐり書きみたいになりましたが…)。 もちろん、COBOLとJavaの違いなんて、こんなものではありません。あくまで基礎の一部を書いたまでになります。 ですが、この3シリーズ(導入・構造編・本編)で、マイグレーションに従事する方やこれから業務がCOBOL⇔オープンに変わる方に、少しでも支えになれば幸いです。 以上で【俺的】COBOL→Java言語移行ガイドを全編終了します。ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apache POIを使って列の幅の自動調整を行う

Excelの列の幅の自動調整に相当することをApache POIで実現したい場合、Sheet.autoSizeColumnを利用すると良いです。ただし、とくに日本語が混ざっている場合、Sheet.autoSizeColumnがうまく列の幅を調整してくれないことがあります。これを回避したい場合、自動調整したい列のセルのフォントを日本語対応のものに設定してやると、想定通り動くようです。 以下はそのサンプルになります。 Workbook workbook = ...; Sheet sheet = ...; // 幅の自動調整をしたい列のセルのフォントを日本語対応のものに設定する。 // (ここでは例としてYu Gothic UIを使う) Cell cell = ...; CellStyle cellStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontName("Yu Gothic UI"); cellStyle.setFont(font); cell.setCellStyle(cellStyle); // 幅の自動調整を行う (ここでは例としてB列とする) CellReference cellReference = new CellReference("B"); sheet.autoSizeColumn(cellReference.getCol()); 環境情報: pom.xml抜粋 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/org.apache.poi/poi --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.1.0</version> </dependency> </dependencies>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apache POIを使ってセルにリンクを設定する

Apache POIを使ってセルにリンクを設定する場合のサンプルを以下に示します。 Workbook workbook = ...; Cell cell = ...; // セルにハイパーリンクを設定する Hyperlink hyperlink = workbook.getCreationHelper().createHyperlink(HyperlinkType.DOCUMENT); hyperlink.setAddress("destination!A1"); cell.setHyperlink(hyperlink); // セルに値を設定する cell.setCellValue("destinationシートのA1へのリンク"); // 文字色を青色にして、アンダーラインを設定する CellStyle cellStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setColor(IndexedColors.BLUE.getIndex()); font.setUnderline(Font.U_SINGLE); cellStyle.setFont(font); cell.setCellStyle(cellStyle); Excelではリンクを自動設定すると、文字色が青色になってアンダーラインが設定されますが、POIでは自動設定されないため、ここもコーディングする必要があります。したがって文字色とアンダーラインの設定が不要な場合は、サンプルの最後のほうは無視してOKです。 なおこのサンプルを実行したイメージは以下のようになります。 環境情報: pom.xml抜粋 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.source>11</maven.compiler.source> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/org.apache.poi/poi --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.1.0</version> </dependency> </dependencies>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringSecurityとSpringBootでログイン認証と投稿機能を実装する

概要 SpringSecurityでDBログイン認証後にユーザーの情報を取得し、投稿した際にユーザー情報も投稿用のテーブルに保存するアプリケーションです。 環境 SpringBoot 2.5.6 Java11 SpringSecurity MySQL SpringDataJPA Gradle 作成する機能 ユーザー登録 ログイン ログアウト 投稿 実装 データベース usersテーブル id email name password created_at int varchar varchar varchar datetime NOT NULL NOT NULL, UNIQUNESS NOT NULL NOT NULL NOT NULL PRIMARY KEY tweetsテーブル id text image user_id created_at updated_at int varchar varchar int datetime datetime NOT NULL NOT NULL NOT NULL NOT NULL NOT NULL PRIMARY KEY エンティティ usersテーブルのエンティティ usersテーブルのエンティティの実装は以下の通りです。 メールアドレスのみ一意性の制約を設けいています。この制約のバリデーションについては、以下に解説していますので、参照ください。 SpringBootで一意性制約のアノテーションを自作(コピペで完成) MUser package com.example.demo.entity; import java.time.LocalDateTime; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.hibernate.annotations.CreationTimestamp; import lombok.Data; @Entity @Data @Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email")}) public class MUser { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer userId; @Column(unique = true) private String email; @Column(name = "name") private String username; private String password; @CreationTimestamp @Column(name = "created_at") private LocalDateTime createTime; private String role; } tweetsテーブルのエンティティ 以下がtweetsテーブルのエンティティクラスです。 注目するところはアソシエーションを組んでいるところです。 今回の場合は、usersが親テーブルとなる多対1の関係なので、@ManyToOneをつけています。 また、@JoinColumnでは、name属性にtweetsテーブルのuser_idを指定し、referencedColumnName属性に参照先であるusersテーブルのidを指定しています。 MTweet package com.example.demo.entity; import java.time.LocalDateTime; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import lombok.Data; @Entity @Data @Table(name = "tweets") public class MTweet { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer tweetId; private String text; @Column(name = "image") private String imageUrl; @Column(name = "user_id") private Integer userId; @CreationTimestamp @Column(name = "created_at") private LocalDateTime createTime; @UpdateTimestamp @Column(name = "updated_at") private LocalDateTime updateTime; @ManyToOne(optional = true) @JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false) private MUser user; } リポジトリ リポジトリにはメールアドレスで認証するため、メールアドレスで検索するメソッドを追加してます。 UserRepository package com.example.demo.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.example.demo.entity.MUser; @Repository public interface UserRepository extends JpaRepository<MUser, Integer> { public Optional<MUser> findByEmail(String email); } サービス 参考文献にはありませんが、自分の場合はサービスクラスを作成しました。理由としては、この後の実装で、登録処理の記述をコントローラーでなく、サービスに記述するためです。 ロジックをサービスに書くことでコントローラーの肥大化を避けることができます。 UserService package com.example.demo.service; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.example.demo.entity.MUser; import com.example.demo.form.SignupForm; import com.example.demo.repository.UserRepository; @Service public class UserService { @Autowired private UserRepository userRepository; public Optional<MUser> findByEmail(String email) { return userRepository.findByEmail(email); } } セキュリティ 以下がセキュリティの設定クラスです。 基本、コメントに書いたままですが、重要なのは最下部の認証の設定です。 ここでは、UserDetailsとPasswordEncoderで認証するように設定しています。 SecurityConfig package com.example.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @EnableWebSecurity @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /**セキュリティ適用外*/ @Override public void configure(WebSecurity web) throws Exception { web .ignoring() .antMatchers("/webjars/**") .antMatchers("/css/**") .antMatchers("/js/**"); } /**直リンクの設定*/ @Override protected void configure(HttpSecurity http) throws Exception { //ログイン不要ページの設定 http .authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/signup").permitAll() .antMatchers("/login").permitAll() .anyRequest().authenticated(); //ログイン処理 http .formLogin() .loginProcessingUrl("/login")//ログイン処理のパス .loginPage("/login")//ログインページの指定 .failureUrl("/login?error")//ログイン失敗時の遷移先の指定 .usernameParameter("email") .passwordParameter("password") .defaultSuccessUrl("/index", true);//ログイン成功時の遷移先の指定 //ログアウト処理 http .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutUrl("/logout") .logoutSuccessUrl("/login?logout"); } /**認証の設定*/ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder encoder = passwordEncoder(); auth .userDetailsService(userDetailsService) .passwordEncoder(encoder); } } 認証・認可に使用するユーザー情報 ここは参考文献にある記事とほぼ同じような実装です。 解説をすると、SpringSecurityが認証・認可に使用するユーザー情報を、リファレンス実装であるorg.springframework.security.core.userdetails.Userを継承して作成しました。 コンストラクタではデータベースから検索したMUserエンティティのインスタンスを受け取り、そのインスタンスのメールアドレス、パスワード、ロールを使って、スーパークラス(org.springframework.security.core.userdetails.User)のコンストラクタを呼び出すという流れです。 ちなみに、org.springframework.security.core.userdetails.Userにはコンストラクタが2つあるようなので、以下に記述しておきます。 今回の実装で使用したのは①の方です。 参考文献の方は、ではboolean型のカラムがあったため、②を使用しています。 User_コンストラクタ① public User(String username, String password, Collection<? extends GrantedAuthority> authorities) ユーザーコンストラクタ② public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) SimpleLoginUser package com.example.demo.service.impl; import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import com.example.demo.entity.MUser; public class SimpleLoginUser extends org.springframework.security.core.userdetails.User { // DBより検索したMUserエンティティ // アプリケーションから利用されるのでフィールドに定義 private MUser user; /** * データベースより検索したuserエンティティよりSpring Securityで使用するユーザー認証情報の * インスタンスを生成する * * @param user userエンティティ */ public SimpleLoginUser(MUser user) { super(user.getEmail(), user.getPassword(), convertGrantedAuthorities(user.getRole())); this.user = user; } public MUser getUser() { return user; } /** * カンマ区切りのロールをSimpleGrantedAuthorityのコレクションへ変換する * * @param roles カンマ区切りのロール * @return SimpleGrantedAuthorityのコレクション */ static Set<GrantedAuthority> convertGrantedAuthorities(String roles) { if (roles == null || roles.isEmpty()) { return Collections.emptySet(); } Set<GrantedAuthority> authorities = Stream.of(roles.split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); return authorities; } } ユーザー情報の取得 データベースのusersテーブルから、SpringSecurityが認証・認可に使用するユーザー情報(SimpleLoginUser)を取得(生成)するためにUserDetailsServiceインターフェースを実装したサービスクラスを作成する必要があります。 このインターフェースでオーバーライドしないといけないメソッドはloadUserByUsername()です。 メソッド名にByUsernameとありますが、一意の情報を渡さないといけないため、今回はその制約を設けてあるメールアドレスを渡しています。 一意の情報を渡すことで、ユーザーを識別できるため、メールアドレスからMUserエンティティを検索し、Userエンティティを基にユーザー情報(SimpleLoginUser)を生成します。 ちなみに、一意であればユーザー名で検索をかけてもOKです。 SimpleUserDetailsService package com.example.demo.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.repository.UserRepository; import lombok.extern.slf4j.Slf4j; @Service @Slf4j public class SimpleUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; /** * メールアドレスで検索したユーザーのuserエンティティをSimpleLoginUserクラスのインスタンスへ変換する * * @param email 検索するユーザーのメールアドレス * @return メールアドレスで検索できたユーザーのユーザー情報 * @throws UsernameNotFoundException メールアドレスでユーザーが検索できなかった場合にスローする。 */ @Transactional(readOnly = true) @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { assert(email != null); log.debug("loadUserByUsername(email):[{}]", email); return userRepository.findByEmail(email) .map(SimpleLoginUser::new) .orElseThrow(() -> new UsernameNotFoundException("User not found by email:[" + email + "]")); } } ユーザー登録 ユーザーのサービスクラスに登録処理(setUser()メソッド)を記述してます。 UserService package com.example.demo.service; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import com.example.demo.entity.MUser; import com.example.demo.form.SignupForm; import com.example.demo.repository.UserRepository; @Service public class UserService { @Autowired private PasswordEncoder encoder; @Autowired private UserRepository userRepository; @Transactional public void setUser(SignupForm form) { MUser user = new MUser(); user.setUsername(form.getUsername()); user.setEmail(form.getEmail()); //パスワードの暗号化 String rowPassword = form.getPassword(); user.setPassword(encoder.encode(rowPassword)); user.setRole("ROLE_GENERAL"); userRepository.save(user); } public Optional<MUser> findByEmail(String email) { return userRepository.findByEmail(email); } } コントローラー 以下が、ユーザー登録とログインのコントローラーの実装です。 SpringSecurityの場合、ログイン処理はWebSecurityConfig(セキュリティ設定クラス)でロジックを記述できるので、基本は画面遷移の処理のみ記述します。 登録処理ではUserServiceをDIしているため、先程記述した、サービスクラスの登録処理メソッドを呼び出します。 フォームからユーザーネームの入力値を取得しているのは、登録処理後のトップ画面でユーザー名を表示するためです。 UserController package com.example.demo.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import com.example.demo.form.SignupForm; import com.example.demo.service.UserService; import com.example.demo.service.impl.SimpleLoginUser; @Controller @RequestMapping("/") public class UserController { @Autowired private UserService userService; @GetMapping("/signup") public String getSignup(@ModelAttribute("user") SignupForm form) { return "user/signup"; } @PostMapping("/signup") public String postSignup(@Validated @ModelAttribute("user") SignupForm form, BindingResult result) { if (result.hasErrors()) { return "user/signup"; } userService.setUser(form); String username = form.getUsername(); model.addAttribute("username", username); return "tweet/index"; } @GetMapping("/login") public String getLogin() { return "user/login"; } @PostMapping("/login") public String postLogin() { return "redirect:/index"; } 投稿機能の実装 サービスクラス 以下が、投稿機能のロジックですが、注意点としては、投稿者のユーザーIDを保存するといった仕様です。 自分はここの実装に苦労し、約1日半かかり実装しました、、、 重要な点は@AuthenticationPrincipalアノテーションです。このアノテーションを付けたパラメータで、認証しているユーザー(ログインユーザー)の情報を受け取ることができます。 ちなみに、このアノテーションの設定の仕方も2通りあります。 ①の方が使用するクラスを明示的に示せるのでわかりやすいかと思います。 AuthenticationPrincipal① @GetMapping(value = "sample") public String sample(@AuthenticationPrincipal SimpleLoginUser loginUser) { log.info("id:{}, name:{}", loginUser.getUser().getId(), loginUser.getUser().getName()); return "sample"; } AuthenticationPrincipal② @GetMapping(value = "sample") public String sample(@AuthenticationPrincipal(expression = "user") User user) { log.info("id:{}, name:{}", user.getId(), user.getName()); return "sample"; } TweetService package com.example.demo.service; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Service; import com.example.demo.entity.MTweet; import com.example.demo.form.TweetForm; import com.example.demo.repository.TweetRepository; import com.example.demo.service.impl.SimpleLoginUser; @Service public class TweetService { @Autowired private TweetRepository tweetRepository; /**投稿機能*/ public void setTweet(TweetForm form, @AuthenticationPrincipal SimpleLoginUser loginUser) { MTweet tweet = new MTweet(); tweet.setUserId(loginUser.getUser().getUserId()); tweet.setText(form.getText()); tweet.setImageUrl(form.getImageUrl()); tweetRepository.save(tweet); } /**投稿全取得*/ public List<MTweet> findAllTweets(){ return tweetRepository.findAll(); } } コントローラー ここは見ての通りという感じですが、@AuthenticationPrincipalで認証ユーザーを取得し、ユーザー名をビューに渡す処理をしています。 また、投稿処理では、サービスロジックを呼び出し投稿完了画面に遷移するようにしています。 TweetController package com.example.demo.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import com.example.demo.form.TweetForm; import com.example.demo.service.TweetService; import com.example.demo.service.impl.SimpleLoginUser; @Controller @RequestMapping("/") public class TweetController { @Autowired private TweetService tweetService; @GetMapping("/new") public String getNew(@ModelAttribute("tweetForm") TweetForm form, Model model, @AuthenticationPrincipal SimpleLoginUser loginUser) { String name = loginUser.getUser().getUsername(); model.addAttribute("username", name); return "tweet/new"; } @PostMapping("/comfirm") public String postNew(@Validated @ModelAttribute("tweetForm") TweetForm form, BindingResult result, Model model, @AuthenticationPrincipal SimpleLoginUser loginUser) { String name = loginUser.getUser().getUsername(); model.addAttribute("username", name); if (result.hasErrors()) { return "tweet/new"; } tweetService.setTweet(form, loginUser); return "tweet/comfirm"; } } 投稿の一覧表示 最後に投稿の一覧表示をするようトップページにあたる部分にTweetServiceで実装した一覧取得のメソッド(findAllTweets())を呼び出し、ビューに渡します。 UserController package com.example.demo.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import com.example.demo.entity.MTweet; import com.example.demo.form.SignupForm; import com.example.demo.service.TweetService; import com.example.demo.service.UserService; import com.example.demo.service.impl.SimpleLoginUser; @Controller @RequestMapping("/") public class UserController { @Autowired private UserService userService; @Autowired private TweetService tweetService; @GetMapping("/") public String getTop(Model model) { List<MTweet> tweetList = tweetService.findAllTweets(); model.addAttribute("tweetList", tweetList); return "top"; } @GetMapping("/index") public String getIndex(Model model, @AuthenticationPrincipal SimpleLoginUser loginUser) { String name = loginUser.getUser().getUsername(); model.addAttribute("username", name); List<MTweet> tweetList = tweetService.findAllTweets(); model.addAttribute("tweetList", tweetList); return "tweet/index"; } @GetMapping("/signup") public String getSignup(@ModelAttribute("user") SignupForm form) { return "user/signup"; } @PostMapping("/signup") public String postSignup(@Validated @ModelAttribute("user") SignupForm form, BindingResult result, Model model) { if (result.hasErrors()) { return "user/signup"; } userService.setUser(form); String username = form.getUsername(); model.addAttribute("username", username); List<MTweet> tweetList = tweetService.findAllTweets(); model.addAttribute("tweetList", tweetList); return "tweet/index"; } @GetMapping("/login") public String getLogin() { return "user/login"; } @PostMapping("/login") public String postLogin(Model model) { List<MTweet> tweetList = tweetService.findAllTweets(); model.addAttribute("tweetList", tweetList); return "redirect:/index"; } } ビュー リスト形式でビューに渡すため、th:each属性を用いて、繰り返しの表示をするようにしています。 アソシエーションを組んでいるため、投稿に紐づくユーザーの情報を取得し表示することも可能です。 以下が、tweetsテーブルからusersテーブルを参照している部分です。 <span th:text="${tweet.user.username}"></span> content.html <div class="contents row" th:each="tweet: ${tweetList}" layout:fragment="content"> <div class="content_post" style="background-image: url(th:text=${tweet.imageUrl});"> <div class="more"> <span> <img src="'arrow_top.png'"> </span> <ul class="more_list"> <li><a href="#">詳細</a></li> <li><a href="#">編集</a></li> <li><a href="#">削除</a></li> </ul> </div> <p th:text="${tweet.text}"></p> <a href="#"> <span class="name"> 投稿者<span th:text="${tweet.user.username}"></span> </span> </a> </div> </div> 以上で実装は終了です。 私自身、外部のテーブルから認証後のユーザーの情報を取得し、投稿用のテーブルにuser_idとして保存するという処理にかなり時間を労しました。 こういう投稿メインのアプリケーションではあるあるの実装だと思うので、SpringBootやJavaで実装している人の役に立てば幸いです。 おわりに とにかく、Java・Springのアノテーションは便利ということ。そして、Javaは難しいということを改めて痛感しました。 Ruby on Railsならコントローラーとモデルの記述だけで、実装できるようなところもJavaだと自分でやらないといけないのが難易度の高さかなと思います。 プログラミング言語は言語なので英語やドイツ語のように若干の違いがあるのだなと思います。だからこそ、面白くもあるのですが、、悩む時間も愛しいものです(笑) 参考文献 Spring Security と Spring Bootで最小機能のデモアプリケーションを作成する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

maven-shade-pluginを使おうとしたら「Unsupported class file major version ○○」って言われてビルドできなくなった話

コトの発端 Java 16でMaven使いながらコードを書いていて依存関係を追加する必要があることに気が付いた私はpom.xmlを開いてdependencyを書き加えていた。 その依存関係はrelocationする事を前提としていたので、いつも通りmaven-shade-pluginのバージョン3.2.4を使ってrelocateの処理を書いた。 暫くしてコードを書き終え、ビルドしてみるとなんとエラーが発生!コンソールには Unsupported class file major version 60 とかいうわけのわからないエラーが出ていた。 結局何が原因だったのさ 使用していた依存関係がJava 16を使用していて、かつmaven-shade-pluginのバージョン3.2.4を使っていたこと、そしてその状態でrelocationをしようとしていたことが原因だと私の中で結論付けた。(そのままやんけ) 解決方法は? ①pom.xmlのjava.versionを16にしておく <properties> <java.version>16</java.version> ... </properties> ②maven-shade-pluginのsnapshotが使えるようにpluginRepositoryを追加する <pluginRepositories> <pluginRepository> <id>maven-snapshots</id> <url>https://repository.apache.org/content/repositories/snapshots/</url> </pluginRepository> </pluginRepositories> ③maven-shade-pluginのversionを3.3.0-SNAPSHOTにする <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.3.0-SNAPSHOT</version> ... </plugin> ※もちろんIntelliJ側などのプロジェクトSDKはJava 16にしておくように めんどいね うん。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む