📢 この記事は gemini-2.5-flash によって翻訳されました
事例導入
従業員データを取得して、統一された応答結果を返して、ページに表示するよ。
まず、XMLファイルを解析するためにdom4jの依存関係を追加する必要があるんだ。
1
2
3
4
5
| <dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
|
XML解析用のユーティリティクラスXMLParserUtils、対応するエンティティクラスEmp、XMLファイルemp.xmlを導入するよ。
静的ページファイルをresources/staticに配置するね。
SpringBootプロジェクトの静的リソース(HTML5+CSS+JSなどのフロントエンドリソース)は、デフォルトでclsspath:/static、classpath:/public、clsspath:/resourcesに置かれるんだ。
Mavenの場合、classpathはsrc/main/resourcesになるよ。
Controllerプログラムを書いて、リクエストを処理して、データを応答するんだ(この例ではコードは省略、後述の3層アーキテクチャを使うよ)。
3層アーキテクチャ
上の例のempコードだと、データアクセス、処理ロジック、リクエスト受信と応答が全部一つのControllerに入っちゃってるから、再利用性が低くて保守も難しいんだ。だから、単一責任の原則を満たすためにこれらを分離する必要がある。3層アーキテクチャにすると、コードの再利用性が高まって、保守しやすくなるし、拡張も楽になるよ。
3層アーキテクチャはController、Service、Daoに分かれるよ。
- Controller:制御層で、フロントエンドから送られてきたリクエストを受け取って、処理してからデータを返すんだ。
- Service:ビジネスロジック層で、具体的なビジネスロジックを処理するよ。
- Dao:データアクセス層(Data Access Object)または永続化層で、データアクセス操作、つまりCRUDを担当するんだ。
ブラウザがリクエストを出す -> Controllerがリクエストを受け取ってデータを応答 -> Serviceがロジックを処理 -> Daoがデータアクセス
上の例のempコードは次のように最適化できるよ。
- Controller
1
2
3
4
5
6
7
8
9
| @RestController
public class EmpController {
@RequestMapping("/listEmp")
public Result listEmp(){
EmpService empServiceA = new EmpServiceA();
List<Emp> empList = empServiceA.listEmp();
return Result.success(empList);
}
}
|
- Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // interface
public interface EmpService {
public List<Emp> listEmp();
}
public class EmpServiceA implements EmpService {
private EmpDao empDao = new EmpDaoA();
public List<Emp> listEmp(){
List<Emp> empList = empDao.listEmp();
empList.forEach(emp -> {
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("講師");
} else if ("2".equals(job)) {
emp.setJob("班主任");
} else if ("3".equals(job)) {
emp.setJob("就業指導");
}
});
return empList;
}
}
|
- Dao
1
2
3
4
5
6
7
8
9
10
11
12
| // interface
public interface EmpDao {
public List<Emp> listEmp();
}
public class EmpDaoA implements EmpDao {
public List<Emp> listEmp(){
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList;
}
}
|
レイヤーの疎結合
凝集度:ソフトウェアの各機能モジュール内部における機能的な関連性のこと。
結合度:ソフトウェアの各層/モジュール間の依存関係や関連性の度合いを測るもの。
ソフトウェア設計原則:高凝集度、低結合度
詳細は
https://blog.yexca.net/archives/145
を見てね。
例えば、上の3層アーキテクチャの例だと、ControllerとServiceが結合してて、ServiceとDaoも結合してるよね。
Controllerで直接ServiceオブジェクトEmpService empServiceA = new EmpServiceA();みたいにAを使っちゃうと、Bに変更するときはControllerも変更しなきゃいけないんだ。Serviceを変えてもControllerを変更しないようにするためには、コンテナを作成して、Controllerがコンテナからオブジェクトを取得するように(依存性注入)、そしてServiceがサービスをコンテナに注入するように(制御の反転)できるよ。
- 制御の反転:Inverse Of Control、略してIOC。オブジェクトの生成制御がプログラム自身から外部(コンテナ)に移されること。
- 依存性注入:Dependency Injection、略してDI。コンテナがアプリケーションの実行時に、必要となるリソースを提供するんだ。
- Beanオブジェクト:IOCコンテナで作成、管理されるオブジェクトのこと。
IOC
あるオブジェクトをIOCコンテナに管理してもらうには、対応するクラスに以下のいずれかのアノテーションを付ける必要があるよ。
| アノテーション | 説明 | 位置 |
|---|
@Component | Beanを宣言する基本アノテーション | 以下の3つのカテゴリに属さない場合に使用する(ユーティリティクラスなど) |
@Controller | @Componentの派生アノテーション | コントローラクラスに付ける |
@Service | @Componentの派生アノテーション | ビジネスロジッククラスに付ける |
@Repository | @Componentの派生アノテーション | データアクセス層のクラスに付ける(MyBatis連携ではあまり使わない) |
- Beanを宣言するときは、
value属性で名前を指定できるよ。指定しない場合は、デフォルトでクラス名の先頭が小文字になるんだ。 - SpringBootでのWeb開発では、コントローラBeanの宣言には
@Controllerしか使えないよ。
Beanコンポーネントスキャン
上で宣言したBeanの4つのアノテーションは、有効にするにはコンポーネントスキャンアノテーション@ComponentScanでスキャンされる必要があるんだ。
このアノテーションは、実は起動クラスのアノテーション@SpringBootApplicationにすでに含まれてるんだ。デフォルトのスキャン範囲は、起動クラスのあるパッケージとそのサブパッケージになるよ。
valueまたはbasePackage属性でスキャン範囲を指定できるよ。
1
| @ComponentScan({"dao","net.yexca"})
|
例
上の例では、
- Controller
@RestControllerアノテーションはすでに@Controllerを含んでいるから、修正は不要だよ。
- Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // interface
public interface EmpService {
public List<Emp> listEmp();
}
// 実装クラスに追加
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceA implements EmpService {
private EmpDao empDao = new EmpDaoA();
public List<Emp> listEmp(){
List<Emp> empList = empDao.listEmp();
empList.forEach(emp -> {
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("講師");
} else if ("2".equals(job)) {
emp.setJob("班主任");
} else if ("3".equals(job)) {
emp.setJob("就業指導");
}
});
return empList;
}
}
|
- Dao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // interface
public interface EmpDao {
public List<Emp> listEmp();
}
// 実装クラスに追加
@Repository // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpDaoA implements EmpDao {
public List<Emp> listEmp(){
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
return empList;
}
}
|
DI
@Autowiredアノテーションを使うと依存性を注入できるんだけど、デフォルトでは型に基づいて行われるから、同じ型のBeanが複数あるとエラーになるんだ。
上記の例にDIを追加するよ(変更されたコードはコメントアウトするね)。
- Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @RestController
public class EmpController {
@Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
private EmpService empService;
@RequestMapping("/listEmp")
public Result listEmp(){
//EmpService empServiceA = new EmpServiceA();
//List<Emp> empList = empServiceA.listEmp();
List<Emp> empList = empService.listEmp();
return Result.success(empList);
}
}
|
- Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // interface
public interface EmpService {
public List<Emp> listEmp();
}
// 実装クラスに追加
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceA implements EmpService {
@Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
private EmpDao empDao;
// private EmpDao empDao = new EmpDaoA();
public List<Emp> listEmp(){
List<Emp> empList = empDao.listEmp();
empList.forEach(emp -> {
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("講師");
} else if ("2".equals(job)) {
emp.setJob("班主任");
} else if ("3".equals(job)) {
emp.setJob("就業指導");
}
});
return empList;
}
}
|
- Dao
データ注入がないから、修正は不要だよ。
@Primary
もしServiceにEmpServiceBという別の実装クラスを追加して、それにも@Autowiredを使ったら、プログラムはエラーになるんだ。その場合は@Primaryアノテーションを使って、どれを使うか指定できるよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| // interface
public interface EmpService {
public List<Emp> listEmp();
}
// 実装クラスに追加
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceA implements EmpService {
@Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
private EmpDao empDao;
// private EmpDao empDao = new EmpDaoA();
public List<Emp> listEmp(){
List<Emp> empList = empDao.listEmp();
empList.forEach(emp -> {
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("講師");
} else if ("2".equals(job)) {
emp.setJob("班主任");
} else if ("3".equals(job)) {
emp.setJob("就業指導");
}
});
return empList;
}
}
@Primary // このBeanを使う
@Service // このクラスをIOCコンテナに管理させ、IOCコンテナのBeanにする
public class EmpServiceB implements EmpService {
@Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
private EmpDao empDao;
// private EmpDao empDao = new EmpDaoA();
public List<Emp> listEmp(){
List<Emp> empList = empDao.listEmp();
empList.forEach(emp -> {
String gender = emp.getGender();
if("1".equals(gender)){
emp.setGender("男");
} else if ("2".equals(gender)) {
emp.setGender("女");
}
String job = emp.getJob();
if("1".equals(job)){
emp.setJob("講師");
} else if ("2".equals(job)) {
emp.setJob("班主任");
} else if ("3".equals(job)) {
emp.setJob("就業指導");
}
});
return empList;
}
}
|
@Qualifier
どのBeanを使うか指定するアノテーションで、Controllerで使うよ。上の@Primaryアノテーションはキャンセルするね。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @RestController
public class EmpController {
@Qualifier("empServiceA") // Bean `empServiceA`を使う
@Autowired // 実行時、IOCコンテナがこの型のBeanオブジェクトを提供し、この変数に代入するんだ -- 依存性注入
private EmpService empService;
@RequestMapping("/listEmp")
public Result listEmp(){
//EmpService empServiceA = new EmpServiceA();
//List<Emp> empList = empServiceA.listEmp();
List<Emp> empList = empService.listEmp();
return Result.success(empList);
}
}
|
@Resource
@Qualifierと似てるんだけど、@Autowiredは使わないよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| @RestController
public class EmpController {
// @Qualifier("empServiceA") // Bean `empServiceA`を使う
// @Autowired
@Resource(name = "empServiceB") // Bean `empServiceB`を使う
private EmpService empService;
@RequestMapping("/listEmp")
public Result listEmp(){
//EmpService empServiceA = new EmpServiceA();
//List<Emp> empList = empServiceA.listEmp();
List<Emp> empList = empService.listEmp();
return Result.success(empList);
}
}
|
@Autowiredと@Resourceの違い
@AutowiredはSpringフレームワークが提供するアノテーションで、@ResourceはJDKが提供するアノテーションだよ。
@Autowiredはデフォルトで型に基づいて注入するけど、@Resourceはデフォルトで名前に基づいて注入するんだ。