SpringBootで学ぶDI(Dependency injection)とAOP(アスペクト指向)
🦑 まえがき
ぼくは別のブログで、10年前に AOP
に関する記事を書いてましたが、
最もシンプルにJavaのAOPを書いてみる→そしてJavaScriptへ at HouseTect, JavaScriptな情報をあなたに
久々に SpringBoot
で再入門してみました。
理由としては、だいぶ時間もたち忘れてしまったのもあり、いまなりの書き方を再度覚えたいなーと思ったからになります。
検証しながら書いていきますが、もし間違っている点などありましたら、 hisasann@DJ lemon-Sour🍌 で指摘いただけますと幸いです。
また、サンプルコードがちょいちょい出てきますが、すべてを push したリポジトリと gradle を使ったビルド手順を後ろのほうに載せていますので、合わせてご覧ください。
🥦 DI(Dependency injection)とは
そもそもですが、 AOP を単体で使うことは少なく、この DI + AOP
で採用されることが多いかと思います。
DI というのは、
依存性の注入(いそんせいのちゅうにゅう、英: Dependency injection)とは、
コンポーネント間の依存関係をプログラムのソースコードから排除するために、
外部の設定ファイルなどでオブジェクトを注入できるようにするソフトウェアパターンである。
英語の頭文字からDIと略される。
文章で読むと難解なイメージがありますが、早い話自分で new をしてインスタンスを生成するのではなく、誰か
にインスタンスを生成してもらって、そのインスタンスを使いたいクラスのメンバに 注入 してもらうことをいいます。
このときの 誰か
が Springframework になります。
Springframework が DIコンテナ から必要なインスタンスを取り出して、依存関係を注入してくれます。
Dependency injection
という言葉ができる前は、 IoCコンテナ[制御の反転(Inversion of Control)]
と呼ばれていたようですが、ぼくが依存性の注入を知ったときはすでに、 Dependency injection という言葉が主流でした。
このあたりに関しはては、 Inversion of Control コンテナと Dependency Injection パターン こちらの記事がとても参考になりました。
では DI を使った場合と使わない場合のサンプルコードを見ていきましょう。
🥯 DIを使わないサンプルコード
DI を使わないと自分で new
する必要が出てきます。
この場合、 AOPMain
クラスは AOPControllerImpl
クラスに依存しています。
public class AOPMain {
private IAOPController controller;
public static void main(String[] args) {
controller = new AOPControllerImpl();
controller.uploadFile();
controller.downloadFile();
}
}
🌭 DIを使ったサンプルコード
DI を使うと自分で new
する必要はなく、 controller メンバに勝手に AOPControllerImpl
を Springframework が注入してくれます。
また、 IAOPController
はインターフェイスなので、クラスには依存せず、あくまでもインターフェイスが提供するメンバを通じてアクセスすることになります。
こうすることで、疎結合となります。
またこの場合、 AOPMain
クラスは AOPControllerImpl
クラスに依存していないので、実際にどんなクラスが注入されるかはわかりません。
@SpringBootApplication
public class AOPMain implements CommandLineRunner {
@Autowired
private IAOPController controller;
public static void main(String[] args) {
SpringApplication.run(AOPMain.class, args);
}
@Override
public void run(String... args) throws Exception {
controller.uploadFile();
controller.downloadFile();
}
}
🍣 Bean定義ファイルを使った場合のサンプルXML
今回は、 Bean 定義ファイル
を使う方法と、コードに書く方法がありますが、今回はコードに書く方法で書いています。
もし、 Bean 定義ファイルのほうで書くなら、例ではありますが、以下のようになります。
<beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>
🍙 ここまでを踏まえて「制御の反転」を考える
冒頭で、デザインパターンの 制御の反転(IoCコンテナ)
について書きましたが、サンプルコードを見たあとだと理解がしやすいかもしれません。
DI を使わなければ、 AクラスがBクラスをインスタンス化するので、制御の方向はAクラス → Bクラス
、
DI を使えば BクラスがクラスAに依存関係を注入するため、制御の方向はAクラス ← Bクラス
という感じになり、反転します。
SpringのDIとAOPについてまとめる。 - Qiita
🍥 AngularのDependency injection
実は Angular
はできた当初から DI を採用していて、以下のページに詳細が書かれています。
また、以前調べていたときに、仮引数を固定していたのが面白くてそれについて以下に書いてありますので、 angular の引数で依存オブジェクトを渡す仕組みがわかるかもしれません。(古い情報なので、いまは変わっているかもです)
AngularJS’s tutorial - あなたとともにAngularJS
の $scopeなど、AngularJSがどうやって仮引数を固定しているか
が該当の部分です。
🍝 AOP(アスペクト指向)とは
DI だけでもまぁまぁなボリュームでしたが、ここからがいよいよ本命の AOP
になります。
そもそも、アスペクト指向とは、
アスペクト指向プログラミング
(アスペクトしこうプログラミング、Aspect Oriented Programming、AOP)は、
オブジェクト指向ではうまく分離できない特徴(クラス間を横断 (cross-cutting) するような機能)を
「アスペクト」とみなし、
アスペクト記述言語をもちいて分離して記述することでプログラムに柔軟性をもたせようとする試み。
アスペクトの例としては、データ転送帯域の制限や例外の処理などがある。
Java にアスペクト指向的要素を追加したAspectJ が実験的に実装されている。
オブジェクト指向プログラミングとは直交するプログラミングパラダイムである。
既存のプログラミングパラダイムを置き換えるものではなく、あくまで既存の言語の補助機能として
利用されることを目的としている。
via アスペクト指向プログラミング - Wikipedia
と、なっております。
この オブジェクト指向ででうまくいかなかった部分を埋めるためにアスペクト指向が生まれた
という感じがぼくはすごく好きです。
🍜 横断的関心事(クラス間を横断 (cross-cutting) するような機能)
AOP を語る上で重要なのがこの 横断的関心事 です。
開発者がひとつのまとまりとして考えるもののことで, 通常はソフトウェアの機能や品質など,
開発者がソフトウェアに作り込む要素を指します.
「ひとつのまとまりとして考える」単位は,開発者の判断に依存します.
たとえば「画面の描画」や「データベースへのアクセス」,「応答性能」など,
開発者が名前を付けて取り扱おうとしているものは,
それぞれ関心事であるといえます.
関心事は,それぞれが明確に独立しているわけではなく,他の関心事と重複する部分があったり,
ある関心事が他の関心事に依存していたりすることもあります.
こうゆう言葉を聞くと、ウッとなりますが、意味としては、
いろんなところに書かれた同じプログラム
を意味します。
ログ処理だったり、トランザクション処理だったりと様々ですが、たとえば、 100 個のメソッドがあった場合に、それぞれのメソッドの開始直後にトランザクション開始、メソッドの終わりにトランザクション終了を必ず書かないといけないとすると、もしも一個でもミスをして抜けてしまったら、データベースには commit されません。
このような同じ処理を何回も書きたくないよね、というのもありますが、絶対にやらないといけない横断的処理があった場合にも効力を発揮します。
🧘🏻♂️ SpringframeworkとSpringBootの違い
Springframework は DI
と AOP
で一世風靡しましたが、その後現れた SpringBoot とはどう違うのか、
実は SpringBoot を触るのは今回が初めてだったので、ググった情報で補いますが、
Spring FrameworkはXML形式の設定ファイルを定義することにより設定を外部化することができるが、
Spring Frameworkではこの設定ファイルの設定の煩雑さがしばしば指摘されている。
Spring Bootではこの設定ファイルの複雑さをできる限り排除しており、
必要最低限の設定を行うだけでアプリケーションの起動・実行を行うことができる。
だそうです。
たしかに、 Springframework 時代は XML に結構書かなければいけなかったので、 SpringBoot はアノテーションベースになっているので、書きやすいです。
では DI を使った場合と使わない場合のサンプルコードを見ていきましょう。
🍦 AOPを使わないサンプルコード
このサンプルでの 横断的関心事
は、①と②になります。
ほぼほぼ同じようなコードですが、これが、仮に 100 以上のメソッドに書かないといけないとすると、煩雑ですしミスも起きそうです。
public class AOPController implements IAOPController {
public void uploadFile(jp) {
// ①
System.out.println("AOPControllerクラスのメソッドuploadFileを開始します");
// 重要な処理と仮定
System.out.println("uploadFile");
// ②
System.out.println("AOPControllerクラスのメソッドuploadFileを終了します");
}
public void downloadFile() {
// ①
System.out.println("AOPControllerクラスのメソッドdownloadFileを開始します");
// 重要な処理と仮定
System.out.println("downloadFile");
// ②
System.out.println("AOPControllerクラスのメソッドdownloadFileを終了します");
}
}
🌮 AOPを使ったサンプルコード
AOP を使うと煩雑な処理が実装コードに含まれません。
そして、クラスが一つ増えて、 AOP の機能だけをもったクラスが増えています。
@Component("aop.AOPController")
@Controller
public class AOPController implements IAOPController {
public void uploadFile() {
// 重要な処理と仮定
System.out.println("uploadFile");
}
public void downloadFile() {
// 重要な処理と仮定
System.out.println("downloadFile");
}
}
もともとあった煩雑な処理はこちらに移動しています。
このようにアノテーションをつけて、どこどこの Component
のなんてメソッドが実行されたら何をしてほしいかを全く別ファイルに外だしすることができます。
@Component
@Aspect
public class AOPComponent {
@Before("execution(* aop.*Controller.*(..))")
public void before(JoinPoint jp) { // メソッド名は何でもよい
// ①
System.out.println(jp.getSignature().getDeclaringType().getSimpleName() + "クラスの" + jp.getSignature().getName() + "メソッドを開始します");
}
@After("execution(* aop.*Controller.*(..))")
public void after(JoinPoint jp) { // メソッド名は何でもよい
// ②
System.out.println(jp.getSignature().getDeclaringType().getSimpleName() + "クラスの" + jp.getSignature().getName() + "メソッドを終了します");
}
}
これが、アスペクト指向です。
実行結果は以下のとおりです。
~/_/js-code/gs-gradle/initial master ● ? ./gradlew run ✔ 1685 18:00:08
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :run
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)
2019-10-30 18:00:15.127 INFO 60888 --- [ main] aop.AOPMain : Starting AOPMain on LAB-N1584.local with PID 60888 (/Users/hisamatsu/_/js-code/gs-gradle/initial/build/classes/java/main started by hisamatsu in /Users/hisamatsu/_/js-code/gs-gradle/initial)
2019-10-30 18:00:15.130 INFO 60888 --- [ main] aop.AOPMain : No active profile set, falling back to default profiles: default
2019-10-30 18:00:15.888 INFO 60888 --- [ main] aop.AOPMain : Started AOPMain in 1.129 seconds (JVM running for 1.438)
AOPControllerクラスのuploadFileメソッドを開始します
uploadFile
AOPControllerクラスのuploadFileメソッドを終了します
AOPControllerクラスのdownloadFileメソッドを開始します
downloadFile
AOPControllerクラスのdownloadFileメソッドを終了します
こうみると、たしかにオブジェクト指向ではカバーしきれなかった部分をうまくアスペクト指向が補っている感がありますよね。
🍩 SpringBootをgradleで動かすまでの道のり
以下を読みながらやっていきました。
Getting Started · Building Java Projects with Gradle
ガイドリポジトリをクローン。
$ git clone https://github.com/spring-guides/gs-gradle.git
$ gs-gradle/initial
gradle を Homebrew でインストール。
$ brew install gradle
$ gradle build
$ ./gradlew build
gradle で Java コードを実行する。
$ ./gradlew run
また、今回作成した build.gradle
は以下のようになりました。
一点ハマったのが、 dependencies
のところに以下記述が必要でした。
Maven Repository: org.springframework.boot » spring-boot-starter-aop » 2.1.6.RELEASE
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'application'
mainClassName = 'aop.AOPMain'
// tag::repositories[]
repositories {
mavenCentral()
}
// end::repositories[]
// tag::jar[]
jar {
baseName = 'gs-gradle'
version = '0.1.0'
}
// end::jar[]
// tag::dependencies[]
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile "joda-time:joda-time:2.2"
testCompile "junit:junit:4.12"
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop
compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.1.6.RELEASE'
}
// end::dependencies[]
今回のサンプルコードは、 springboot-aop-gs-gradle/initial/src/main/java/aop at master · hisasann/springboot-aop-gs-gradle にまとめてあります。
🥙 JavaScriptでAOPを実現する
かなり前ですが、 JavaScript で AOP を実現するコードを書いたので、合わせて貼っておきます。
🍜 あとがき
細かい AOP 用語はほぼ取り上げませんでしたが、 DI + AOP
の面白さは伝えられたのかなーと思っています。
また、オブジェクト指向は、何度勉強しても新しい発見があり、さらにそれプラス、アスペクト指向も再考すると関心しか生まれません。
賢い人の頭の中はどうなっているのかと。
ではでは!
🥪 参考記事
テックノート – @Qualifierを使って任意のIDをつける方法(インタフェースと実クラスが1対nの場合の対処方法)
springの再入門 - DI(依存性注入) - Qiita
SpringのDIとAOPについてまとめる。 - Qiita
Spring FrameworkでDIする3つの方法 - Reasonable Code
[spring boot7]AOPを使ってログ出力してみよう - プログラミング MEMO
ざっくりとSpringで使うAOPの解釈をする - Qiita
GradleでSpring Bootのプロジェクトを環境別にビルドする - Qiita
Spring BootでAOP(アスペクト指向)を使うとコードが奇麗になる(@Aspect,@Before,@Afterなど) 株式会社CONFRAGE ITソリューション事業部