🐡

【初心者向け 102】SpringBoot核心原理2

2023/10/21に公開

DI

この記事は、こちらの講座からまとめた内容をまとめております。

https://www.inflearn.com/course/스프링-핵심-원리-기본편/dashboard

コンストラクタ注入

コンストラクタを呼び出す際に一度のみ呼び出されることが保証され、不変、実習の依存関係に利用するパターン

private final MemberRepository memberRepository;
 private final DiscountPolicy discountPolicy;
 @Autowired
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
 }
}

getter/setter注入

JavaのDTOのようにgetter/setterを利用するパターン。何かの修正が必要な際のみ使います。

private MemberRepository memberRepository;
 private DiscountPolicy discountPolicy;
 @Autowired
 public void setMemberRepository(MemberRepository memberRepository) {
 this.memberRepository = memberRepository;
 }
 @Autowired
 public void setDiscountPolicy(DiscountPolicy discountPolicy) {
 this.discountPolicy = discountPolicy;
 }

フィールド注入

fieldから注入する方法。昔はよくつかわれたようですが、テストをする際に色々な限界があるようです。テストでは使ってもいいですが、Applicationを作成時には使わないこと!(原理は詳しくは理解してないですが、まず基本的には使わない!)

@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}

コンストラクタ注入の場合、Beanの具象体を生成する際に、一回のみ呼び出されるので、不変であり、整合性を守ることが可能です。

また、finalキーワードを入れられるので、不変を確実に保証することができるらしいです。

最近はLombokと活用し、1つのコンストラクタのみある場合、@Autowiredが要らないこと原理を利用してこのようにコードを組みます。

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements  OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

Autowiredの動作原理

Autowiredはまず、classタイプ名でひぱってくるオブジェクトを探します。しかし、インタフェースがあり、具象クラスが2個以上ある場合は、
同じタイプとして認識するので、エラーが生じます。

field, parameter

直接注入する場合は、AppConfigを触りましたが、自動の場合はフィールド名、パラメータ名を変更することで、具象クラスを選ぶことが可能です。

 @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy ratediscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = ratediscountPolicy;
    }

@Qualifier

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy")DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }


Component("")のように@Qualifierに特定な文字列を入れ@Autowiredとマッピングすることができます。

@Primary

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

@Primaryアノテーションを付けることで、自動的にひぱってくることが可能です。

@Primaryは、メイン、@Qualifierは補助DBに利用するらしいです。

Custom Annotation

@Quelifierは文字列なので、コンパイル時、タイプミスをチェックすることができないので、このようにアノテーションを作る方法もあります。

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {

}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy

   @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

Spring Beanのライフサイクル

Spring Container聖性➞ Spring Bean生成 ➞ DI ➞ 初期化 call back ➞ 使用 ➞ 消滅 call back ➞ Spring終了

初期化call back : Bean生成が終わり、DIが完了してから呼び出されるメソッド
例)connect()

消滅前call back : Beanが消滅される前にcall back
例)disconnect(), close()

約1か月前は、いつもDBにアクセスする時にConnectionオブジェクトを新しく生成し、close()で消滅しましたが、Connectionプールで事前にConnetionオブジェクトを生成するConnection Poolで、効率を上げた経験があります。

public class NetworkClient {
 private String url;
 public NetworkClient() {
 System.out.println("Constructor made, url = " + url);
 connect();
 call("初期化コネクト");
 }
 public void setUrl(String url) {
 this.url = url;
 }
 
 //after DI callback
 public void connect() {
 System.out.println("connect: " + url);
 }
 
 public void call(String message) {
 System.out.println("call: " + url + " message = " + message);
 }
 
 //before end callback
 public void disconnect() {
 System.out.println("close: " + url);
 }
}

このような場合はまず生成をするロジックと、urlというsetをするsetterを分離します。

外部URLに繋ぐことは重い作業であり、オブジェクトを生成するところと初期化をするところを分けた方がSRPとメインテナンスにも良いらしいです。

public class BeanLifeCycleTest {
 
 @Test
 public void lifeCycleTest() {
 ConfigurableApplicationContext ac = new
AnnotationConfigApplicationContext(LifeCycleConfig.class);
 NetworkClient client = ac.getBean(NetworkClient.class);
 ac.close(); 
 }
 
 @Configuration
 static class LifeCycleConfig {
 
  @Bean
  public NetworkClient networkClient() {
  NetworkClient networkClient = new NetworkClient();
  networkClient.setUrl("http://hello-spring.dev");
  return networkClient;
  }
 }
}
Constructor made, url = null
connect: null
call: null message = 初期化コネクト

この場合は、先ほどのsetterメソッドで直接初期化をする必要があります。

しかし、前述したどおり、springは初期化call back, 消滅前call backを保証する3つの方法があります。

Springのインタフェース活用(昔の方法)

public class NetworkClient implements InitializingBean, DisposableBean 

/*before
 public NetworkClient() {
 System.out.println("Constructor made, url = " + url);
 connect();
 call("初期化コネクトmessage");
 }*/
 
 /*after
 public NetworkClient() {
 System.out.println("Constructor made, url = " + url);
 }
 */
 
  @Override
 public void afterPropertiesSet() throws Exception {
 connect();
 call("初期化コネクトmessage");
 }
 @Override
 public void destroy() throws Exception {
 disConnect();
 }

この後、BeanLifeCycleTestを実行すれば、以下のような結果になります。

 @Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new
AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close(); 
}

@Configuration
static class LifeCycleConfig {

  @Bean
 public NetworkClient networkClient() {
 NetworkClient networkClient = new NetworkClient();
 networkClient.setUrl("http://hello-spring.dev");
 return networkClient;
 }
Constructor made, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 初期化コネクトmessage
13:24:49.043 [main] DEBUG 
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing NetworkClient.destroy
close + http://hello-spring.dev

@Bean(initMethod =, destroyMethod=)

先ほどのinterfaceのimplements,importを取消し, @overrideのメソッドの以下のように修正します。

public void init()  {
       connect();
       call("初期化コネクトmessage");
   }

   
   public void close()  {
       disconnect();
   }
}

その後、Spring ContainerのBeanのアノテーションを修正し、マッピングします。

      @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
Constructor made, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 初期化コネクトmessage
13:33:10.029 [main] DEBUG 
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing NetworkClient.close

外部ライブラリもそのまま適用できることが長所らしいです。

@PostConstruct, @PreDestroy(今のやり方)

先ほどのinit、closeメソッドにアノテーションをつき、Spring containerの @Bean(initMethod = "init", destroyMethod = "close")を元に戻せば、完了です。

@PostConstruct
   public void init()  {
       connect();
       call("初期化コネクトmessage");
   }

   @PreDestroy
   public void close()  {
       disconnect();
   }

javaxというjavaから直接提供するアノテーションなので、Springでも依存せず、他のContainerにも利用することが可能です。

結論

codeを修正できないライブラリの場合は、@Bean(initMethod =, destroyMethod=)を、普段は@PostConstruct, @PreDestroyを利用しましょう。

Prototype Scope

Springは基本的にSingtonのbeanを生成し、生成から、初期化、消滅のライフサイクルを管理します。
しかし、Prototypeというタイプは毎度新しいオブジェクトを生成し、生成と初期化まで管理します。@PreDestroyのように、消滅に対しては管理せず、クライアントに任せるようです。

Singleton
@Test
    void singletonBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }

    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy(){
            System.out.println("SingletonBean.destroy");
        }
    }
}
singletonBean1 = hello.core.scope.SingtonTest$SingletonBean@5c2375a9
singletonBean2 = hello.core.scope.SingtonTest$SingletonBean@5c2375a9
10:29:13.367 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@7bd4937b, started on Thu Oct 19 10:29:12 KST 2023
SingletonBean.destroy
prototype
@Test
    void prototypeBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
	
	ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeBeanTest$PrototypeBean@5c2375a9
prototypeBean2 = hello.core.scope.PrototypeBeanTest$PrototypeBean@60129b9a
11:24:02.359 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@7bd4937b, started on Thu Oct 19 11:24:01 KST 2023

Process finished with exit code 0

prototypeの場合、@5c2375a9、@60129b9aでメモリアドレスが異なり、SingletonBean.destroyのように消滅時に@PreDestoryアノテーションを付けたが呼System.out.println("PrototypeBean.destroy")が呼びだされないことが分かります。

SingtonとPrototypeを平行する際にの問題点

例えば、SingletonのbeanがPrototypeを依存する場合は以下のような問題点があります。



static class ClientBean {
	 private final PrototypeBean prototypeBean;
	 @Autowired
 
	 public ClientBean(PrototypeBean prototypeBean) {
	 this.prototypeBean = prototypeBean;
	 }
 
	 public int logic() {
	 prototypeBean.addCount();
	 int count = prototypeBean.getCount();
	 return count;
	 }
 }

static class PrototypeBean{
        private int count = 0;

        public  void addCount(){
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }

本来なら、Prototype beanは毎度異なるインスタンスとして、生成されますが、
singletonが依存する場合は以下のような結果になります。

本来なら、独立したインスタンスに存在しているはずですが、addCount()をするとそれぞれ1,1ではなく2になることが分かります。
結論から言えば、これは前述したSpring Beanのライフサイクルと関係があります。
通常のSpring beanは、singletonなので、メモリ上に割り当たり、システムが終了されるまで、メモリに残ります。より詳しく話してみますと、singletonのbeanは生成した後、DIが行われますので、この場合はPrototypeも使用前にsingletonのfieldとして存在します。

結局、使用するたびに新しいproto beanを生成させるように解決方法が必要になります。

解決法1 DL(Dependancy Lookup)

Springに依存して今うパターンでオススメしない方法です。

@Autowired
private ApplicationContext ac;

public int logic() {
 PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
 prototypeBean.addCount();
 int count = prototypeBean.getCount();
 return count;
}

毎度、SpringcontainerからPrototypeBeanを生成します。

解決法2 ObjectProviderをDI

Singletonで、Prototypeを依存するBeanなら、fieldでObjectProvider<T> というクラスを追加し、タイプパラメータにPrototypeBeanを入れます。この方法は様々な機能を備えていますが、Springに依存する短所もあります。


static class ClientBean {
	@Autowired
	private ObjectProvider<PrototypeBean> prototypeBeanProvider;

	public int logic() {
	 PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
	 prototypeBean.addCount();
	 int count = prototypeBean.getCount();
	 return count;
	}
}	

解決法3 JSR-330(外部ライブラリ、gradleに追加)

Javaの標準技術ですが、機能がすくない!テストなどによく使われます。

static class ClientBean {
        @Autowired
        private Provider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        } 
   }

Web Scope

web scopeはweb環境でのみ発生するscopeであり、requestが代表的なweb scopeです。
requestはHttpのrequestがあるたびに生成されるbeanで、一つのHttp request当たり一つのbeanが原則です。 requestが終われば、消滅されます。
web scopeの種類として、session,applicationというscopeも存在します。

UUIDという固有のIDなどと活用し、ログに活用するらしいです!

@Component
@Scope(value = "request")
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message){
        System.out.println("["+ uuid +"]" + "["+ requestURL +"]"  +  message);
    }

    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("["+ uuid +"] request scope bean create:" + this );
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProviderr;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProviderr.getObject();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}


@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

    public void logic(String id){
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id );
    }
}

また、Logが使ったことがないのですが、get要請でurlを入力する際にrequestが発生するのでuuidなどとmessageが出力されることが分かりました。これがログかい!

[69e6940a-80de-42c3-bba3-b64a56c1d92f] request scope bean create:hello.core.common.MyLogger@75d3823f
[69e6940a-80de-42c3-bba3-b64a56c1d92f][http://localhost:8080/log-demo]controller test
[69e6940a-80de-42c3-bba3-b64a56c1d92f][http://localhost:8080/log-demo]service id = testId
[69e6940a-80de-42c3-bba3-b64a56c1d92f] request scope bean close:hello.core.common.MyLogger@75d3823f

F5でrefreshをすれば、UUIDが変わります!不思議だ!!

[d2fc8599-c46a-4037-81e3-c7564dbb9f07] request scope bean create:hello.core.common.MyLogger@2e50bf8e
[d2fc8599-c46a-4037-81e3-c7564dbb9f07][http://localhost:8080/log-demo]controller test
[d2fc8599-c46a-4037-81e3-c7564dbb9f07][http://localhost:8080/log-demo]service id = testId
[d2fc8599-c46a-4037-81e3-c7564dbb9f07] request scope bean close:hello.core.common.MyLogger@2e50bf8e

Proxy(代理応答)

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS )
public class MyLogger {

    private String uuid;
    private String requestURL;

先ほどのWeb scopeのbeanにProxyModeを追加すれば、クライアントのコードを修正せずに、作動させることができます。

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d

Singtonのbeanもこのように変な符号が使っていますが、バイトコードを操作し、MyLoggerを継承した偽物のproxy objectを生成します。(CGLIBという仮想のライブラリからバイトコードを操作)

このProxy objectが実際のrequestが入るたび、実際のbean(@scope(value="request"))であるMyLoggerのDLを行います。

Discussion