Open14

Spring 勉強メモ

Hidden comment
Hidden comment
さにらねさにらね

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 ''

https://yuukiyg.hatenablog.jp/entry/2019/11/04/031750

https://spring.pleiades.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-initializingbean

https://www.techscore.com/tech/Java/Others/Spring/3-3/

BeanFactory

  • BeanFactoryはBeanインスタンスを生成してインジェクションを行う
  • DIコンテナからインスタンスを取得する
    • BeanFactoryからインスタンスを取得する
BeanFactory factory = new ClassPathXmlApplicationContext("context.xml");
SampleLogic logic = factory.getBean(SampleLogic.class);

→基本的にはBeanFactoyの上位であるApplicationContextを使うことになる。
根本ではBeanFactoryが機能しているということ

BeanScope

https://volkruss.com/?p=3215

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

メッセージ

メッセージリソースを使う場合(ただメッセージを使うならこれが良い)

@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通信が行われました");
        }
    }
}

さにらねさにらね

テスト

  • データベースの利用
    • \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);
    }
}

※コード作成中です(感覚こんな感じ)

https://github.com/Freeongoo/spring-boot-dbunit

https://springtestdbunit.github.io/spring-test-dbunit/

アノテーション

  • @SpringBootTestアノテーションを付けることでSpringBootアプリケーションとしてテストが実行できます
    • @Autowiredに対してDI可能になる

https://spring.pleiades.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html

  • @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());
    }
    
}

書き方は色々あります
https://www.baeldung.com/java-spring-mockito-mock-mockbean

テスト

Springとは無関係ですが、テストについて

  • TDD

    • 失敗するコードを書く
    • コンパイルが通り適切に失敗するテストが書けたら次のテストを書く
    • 現在失敗しているテストが通るまで、次の製品コードは書いてはいけない
  • テストコード

    • テストコードは実コードの変更、安全性を保つ
    • 1つのテストに1つのアサート
      • 極力そうあるべき
      • 1つのテストコードに1つの概念がテストできれば良い

MockMvc

ここの内容を試してみてまとめます

https://spring.pleiades.io/guides/gs/testing-web/

@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 レイヤーのみをインスタンス化します。

さにらねさにらね

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);
    }
}

https://qiita.com/tag1216/items/55742fdb442e5617f727

この辺は基底クラスを作った方がいい

https://github.com/jirentaicho/spring-study-sample/blob/c41caf190647aa9fd41f06529614f9babe38c9b0/src/main/java/com/volkruss/misaka/domain/repository/base/BaseRepository.java

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

https://www.baeldung.com/jpa-join-column

サンプル

受注とその詳細

受注エンティティ

@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
さにらねさにらね

コントローラーの引数

色々あるのでまとめてます

  • aaaa
  • aaaaa
  • aaaaa
  • aaa
  • aaa
  • a