Spring 勉強メモ

個人的なメモです。本当にメモです。
パッケージとかグチャグチャですけど挙動の確認用
参考本
- [改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ
- 現場至上主義 Spring Boot2 徹底活用

Bean Life Cycle
SpringContextが以下を通してBeanに依存注入する
- setterメソッド
- コンストラクタインジェクション
- Autowired
多分重要なのがBeanの初期化時と破棄時にカスタムイベントを登録できるということ。
- @PostConstruct
- 初期化処理を行うメソッドに付けるアノテーション
- @PreDestroy
- 終了処理を行うメソッドに付けるアノテーション
コンストラクタでは依存関係が
→ コンストラクタ実行時にはまだ依存注入されていない。
@RestController
public class SampleController{
@Autowired
private SampleLogic sampleLogic;
@PostConstruct
private void postcon(){
// ok
sampleLogic.init("admin");
}
public SampleController(){
// まだ利用できない NullPointerException
this.sampleLogic.init("sample");
}
}
※DIコンテナによって変数にインジェクションされた後に呼ばれているため
タイミング的にはApplicationコンテキストの初期化が終わりましたの後(ここ)です
2022-09-11 14:21:51.699 INFO 11748 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 732 ms
admin
2022-09-11 14:21:51.939 INFO 11748 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
BeanFactory
- BeanFactoryはBeanインスタンスを生成してインジェクションを行う
- DIコンテナからインスタンスを取得する
- BeanFactoryからインスタンスを取得する
BeanFactory factory = new ClassPathXmlApplicationContext("context.xml");
SampleLogic logic = factory.getBean(SampleLogic.class);
→基本的にはBeanFactoyの上位であるApplicationContextを使うことになる。
根本ではBeanFactoryが機能しているということ
BeanScope
SpringBootでBeanの取得
- @Configurationアノテーションを付与したコンフィグクラスを作成する
- コンフィグクラスで@Beanアノテーションを付与したメソッドでBeanとして管理するクラスをインスタンス化して返却する
Bean化したいクラスは特段設定することはない
public class MyBean {
private final String name;
MyBean(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
コンフィグクラス
@Configuration
public class BeanConfig {
@Bean
public MyBean myBean(){
return new MyBean("Admin");
}
}
- 利用する時(インジェクションする時)はどっちでもOK(通常はアノテーションを利用する)
- ApplicationContextを使う
- @Autowiredを使う
ApplicationContext
@RestController
public class SampleController{
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/sample")
public String aaaa(){
MyBean myBean = applicationContext.getBean(MyBean.class);
System.out.println(myBean.getName());
return "ok";
}
}
@Autowired
@RestController
public class SampleController{
@Autowired
private MyBean myBean;
@GetMapping("/sample")
public String aaaa(){
System.out.println(myBean.getName());
return "ok";
}
}

ApplicationContext
- ApplicationContextはBeanFactoryを拡張したもの
- 以下のようなものがBeanFactoryに追加されている
- Bean定義ファイルの読み込み
- メッセージソース
- イベント処理
SpringでBean定義を読み込むとき
- xmlファイルを指定する
- XmlWebApplicationContext
- JavaConfigを読みこむ
- AnnotationConfigWebApplicationContext
- web.xmlに記載が必要
- <init-param>
- SpringBootならアノテーションを付けるだけでOK
- AnnotationConfigWebApplicationContext
メッセージ
メッセージリソースを使う場合(ただメッセージを使うならこれが良い)
@RestController
public class SampleController{
@Autowired
private MessageSource messageSource;
@GetMapping("/sample")
public String aaaa(){
System.out.println(messageSource.getMessage("sample", new String[] {}, null));
return "ok";
}
}
ApplicationContextを使う場合
@RestController
public class SampleController{
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/sample")
public String aaaa(){
System.out.println(applicationContext.getMessage("sample", new String[] {}, null));
return "ok";
}
}
Beanの取得
一つ上のコメントの「SpringBootでBeanの取得」参照
イベント処理
ApplicationContextが発生させたイベントを受け取ることができる。
ApplicationContextが発生させるイベント
- ContextRefreshedEvent
- ContextStartedEvent
- ContextStoppedEvent
- ContextClosedEvent
- RequestHandledEvent
- HTTPリクエストによりサービスが呼ばれたときに発生する
イベントを受け取りはApplicationListenerの実装クラスを作り、そのクラスをDIコンテナに登録することで行うことができる。
@Component
public class CustomEventListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if(event instanceof ContextRefreshedEvent){
System.out.println("Beanライフサイクルが初期化されました");
} else if(event instanceof RequestHandledEvent){
System.out.println("http通信が行われました");
}
}
}

Spring AOP
リンク切れの為削除しました

テスト
- データベースの利用
- \src\test\resourcesにapplication.ymlを作成して設定すれば、専用のテストデータベースが利用可能
DBUnit
- 依存モジュールを設定
- implementation 'org.springframework.boot:spring-boot-starter-jdbc'
- runtimeOnly 'com.h2database:h2'
- ここはお好み
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
- testImplementation 'com.github.springtestdbunit:spring-test-dbunit:1.3.0'
- testImplementation 'org.dbunit:dbunit:2.7.3'
- データ投入用のxmlを作成
- src/test/resources/yyy.xml
- 設定ファイルの作成
- テストクラスの作成
※jdbcも必須です
◆設定ファイル
src/test/resourcesに配置します
- application.yaml
- いつも通りのデータベース接続情報
- data.xml
- テスト実行時のデータ
- schema.sql
- これはh2でテーブルを作成しています(必須ではありません)
◆xmlファイルの作成
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user_info name="Admin" />
</dataset>
- user_infoテーブルのname=Adminというレコードを追加する
◆テストクラスの作成
@DatabaseSetup("/data.xml")
@SpringBootTest
@TestExecutionListeners({
TransactionalTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
class UserServiceTest {
@Autowired
private UserRepository userRepository;
@Test
public void test_hoge(){
String username = this.userRepository.get();
assertEquals("Admin",username);
}
}
※コード作成中です(感覚こんな感じ)
アノテーション
- @SpringBootTestアノテーションを付けることでSpringBootアプリケーションとしてテストが実行できます
- @Autowiredに対してDI可能になる
- @InjectMocks
- テスト対象となるクラスに対して付与します
- @Mockでモック化したオブジェクトをテスト対象のクラスに注入します
@SpringBootTest
class LoginCampaignServiceImplTest {
@Mock
private UserDao userDao;
@Mock
private UserContext userContext;
@InjectMocks
private LoginCampaignServiceImpl loginCampaignService;
@BeforeEach
public void setUp(){
MockitoAnnotations.openMocks(this);
// ユーザーID取得の挙動をモックする
when(this.userContext.getId()).thenReturn(1);
}
@Test
public void test_get300(){
Date userLoginDate = this.getDateFromStr("20220701");
Date systemDate = this.getDateFromStr("20220822");
Gold result = null;
// ユーザー取得の挙動をモックする
when(this.userDao.getUser(anyInt())).thenReturn(new User(1,"上条当麻",userLoginDate));
// システム日付取得(staticメソッド)の挙動をモックする
try(MockedStatic<SystemUtil> mock = mockStatic(SystemUtil.class)){
mock.when(SystemUtil::getSystemDate).thenReturn(systemDate);
result = this.loginCampaignService.getLoginReward();
}
assertEquals(300,result.getAmt());
}
}
書き方は色々あります
テスト
Springとは無関係ですが、テストについて
-
TDD
- 失敗するコードを書く
- コンパイルが通り適切に失敗するテストが書けたら次のテストを書く
- 現在失敗しているテストが通るまで、次の製品コードは書いてはいけない
-
テストコード
- テストコードは実コードの変更、安全性を保つ
- 1つのテストに1つのアサート
- 極力そうあるべき
- 1つのテストコードに1つの概念がテストできれば良い
MockMvc
ここの内容を試してみてまとめます
@MockMvcというアノテーションを使うことでWebレイヤーのみをテストすることができる。
@SpringBootTestというアノテーションで大抵の事が動いているように思えます。
例えば以下のHelloServiceクラスを利用したHelloControllerクラスのテストを考えます。
@Component
public class HelloService {
public String getHello(){
return "Hello";
}
}
上記のサービスクラスを利用したコントローラークラス
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello(){
return this.helloService.getHello() + " FROM HelloController";
}
}
TestRestTemplateを利用して、http通信を行ったテストをしてみます
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate testRestTemplate;
@Test
public void test_hello_message(){
assertThat(this.testRestTemplate.getForObject("http://localhost:" + port + "/hello", String.class)).contains("Hello FROM HelloController");
}
}
- TestRestTemplateを使う時は@LocalServerPortを利用するようです
- SpringBootTest.WebEnvironment.RANDOM_PORTでランダムなポートで起動
@SpringBootTestを使っているのでサーバーも起動していることがわかります。
次に@MockMvcを利用した場合です
※staticインポートしています
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void test_hello_message() throws Exception {
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("Hello FROM HelloController"));
}
}
HelloServiceがないよって怒られます(サービスクラスがAutowiredできていない)
Field helloService in com.volkruss.swaggersample.controller.HelloController required a bean of type 'com.volkruss.swaggersample.controller.HelloService' that could not be found.
@MockBeanを利用してサービスクラスをモック化します
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private HelloService helloService;
@Test
public void test_hello_message() throws Exception {
when(helloService.getHello()).thenReturn("Hello");
this.mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("Hello FROM HelloController"));
}
}
@MockMvcを使った場合は
Spring Boot はコンテキスト全体ではなく Web レイヤーのみをインスタンス化します。

SpringSecurity

JPA
依存
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Entityクラスと紐づけて利用する
- データベースのテーブルと紐づいたEntityクラスと紐づけて利用できます
- @Idで指定したカラムはfindByIdで検索されるカラムになります
- @Table(name="xxx")でテーブル名を指定します
@Entity
@Table(name="stocks")
public class Stock {
@Id
@Column(name="id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
@Column(name="name")
public String name;
}
JpaRepository<T,ID>を継承したインターフェースを作成する
- IDはfindByIdで検索できる型を指定します。
- idならInteger
- nameならString
import org.springframework.data.jpa.repository.JpaRepository;
public interface StockJpa extends JpaRepository<Stock,Integer> {
}
独自に用意したリポジトリなどから利用する
- JpaRepository<T,ID>を継承したインターフェースをDIする
@Repository
public class StockRepository {
@Autowired
private StockJpa stockJpa;
public List<Stock> findAll(){
return this.stockJpa.findAll();
}
public Stock findById(int id){
return stockJpa.findById(id).orElseThrow();
}
}
DBUnitでテストする
@DatabaseSetup("/data.xml")
@SpringBootTest
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
class UserServiceTest {
@Autowired
private StockRepository stockRepository;
@Test
public void test_repo(){
Stock stock = this.stockRepository.findById(1);
assertEquals("鉛筆", stock.name);
}
}
※DBUnitについては上記コメント(テスト)にて記載
JPQL
- JPAで利用するクエリのこと
- EntityManagerを利用する
- createQuery
- createNativeQuery
リポジトリクラスにてEntityManagerの利用を行う
- from Stock
- テーブル名でなくクラス名を指定
- クエリにパラメータがあればsetParameterを利用する
@Repository
public class StockRepository {
@Autowired
private EntityManager entityManager;
public Stock findByName(String name){
Stock stock = entityManager
.createQuery("from Stock where name = :name",Stock.class)
.setParameter("name",name)
.getSingleResult();
return stock;
}
}
DBUnit
@DatabaseSetup("/data.xml")
@SpringBootTest
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
class UserServiceTest {
@Autowired
private StockRepository stockRepository;
@Test
public void test_repo_e(){
Stock stock = this.stockRepository.findByName("鉛筆");
assertEquals("鉛筆", stock.name);
}
}
この辺は基底クラスを作った方がいい
h2でpostgreを使う
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL;DATABASE_TO_UPPER=false;
username: sa
password: sa
initialization-mode: always
初期データ
- schema.sql
- data.sql
data.sqlが実行されない
ddl-auto: noneを指定する
spring:
jpa:
hibernate:
ddl-auto: none
いや多分コッチ
spring:
jpa:
defer-datasource-initialization: true
リレーション系
- @ManyToOne
- 多対1
- @OneToMany
- 1対多
- @JoinColumn
- 結合先テーブルのカラム名を指定するアノテーション
- 結合先テーブルのプライマリキーを指定
- @DateTimeFormat
サンプル
受注とその詳細
受注エンティティ
@Entity
@Table(name = "accept")
public class Accept {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
@Column(name = "name")
public String name;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name="accept_id")
public List<AcceptDetail> details;
}
- 1つの受注は複数の詳細を持っています
- 外部キーはaccept_idという明細側のカラムを指定
- fetch = FetchType.EAGER
- 初期値は遅延(LAZY)
- EAGERは即時取得
詳細エンティティ
@Table(name = "accept_detail")
public class AcceptDetail {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
@Column(name = "name")
public String name;
}
今回はJpaRepositoryを介してレコードを取得します
public interface AcceptDetailJpa extends JpaRepository<AcceptDetail,Integer> {
}
レコードは以下のようになっています
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<accept name="受注1" />
<accept name="受注2" />
<accept name="受注3" />
<accept_detail name="パソコン一式" accept_id="1" />
<accept_detail name="冷蔵庫" accept_id="1" />
<accept_detail name="洗濯機" accept_id="1" />
<accept_detail name="パソコン一式" accept_id="2" />
<accept_detail name="プリンター" accept_id="2" />
<accept_detail name="チョコレート" accept_id="3" />
</dataset>
テストをして確認しました。
@DatabaseSetup("/data2.xml")
@SpringBootTest
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
class AcceptServiceImplTest {
@Autowired
private AcceptServiceImpl acceptService;
@Test
public void test_hoge(){
Accept accept = this.acceptService.getAcceptWithEstmate(1);
assertEquals(3,accept.details.size());
}
}
受注に紐づく詳細が取得できています
F
※個人的にはSQLで書いた方が安心する

バリデーション
知っておくこと
- @Validated
- BindingResult
- Validatorインターフェース
- @InitBinder
パラメータの受け取り
- @PathVariable
- @RequestBody

リンク切れの為削除しました。

コントローラー
- @ModelAttribute

コントローラーの引数
色々あるのでまとめてます
- aaaa
- aaaaa
- aaaaa
- aaa
- aaa
- a

リンク先について修正します