2017年12月21日 星期四

Java Spring - Scoping beans


  • Bean
    • 默認情況下,Spring application context所有的bean都是singleton(只有一個instance),也就是不管一個bean被injects多少次,都是同一個instance
    • 問題:
      • 有些application中,object使用stateless可能不太合理
      • 有些時候某些object是mutable(狀態會變),所以會有一些state,因此reuse不安全,這個時候class若是singleton bean就會有問題
    • Spring定義了bean的scope:
      1. Singleton: 整個application只會建立一個instance (default)
      2. Prototype: 每次injects或是透過Spring application context get時都會建一個新的bean instance
      3. Session: Web application中,為每個session創建一個bean instance
      4. Request: Web application中,為每個request創建一個bean instance
    • 如果使用除了singleton以外的scope,就會需要使用@Scope annotation
    • @Scope可以和@Component及@Bean一起使用
      • 例如:
      • @Component
        @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //也可以寫作@Scope("prototype")
        public class Notepad { ... }
        
      • XML:
      • <bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
        
  • 使用request及session scope
    • 假設做一個購物車系統,需要有session判斷是同一個人,這時候就可以使用sessionScope,如下
    • @Component
      @Scope( value=WebApplicationContext.SCOPE_SESSION, // value=session
          proxyMode=ScopedProxyMode.INTERFACES) // 解決了session or request scope的bean injects到singleton bean的問題
      public ShoppingCart cart() { ... }
      
    • 假設要將SoppingCart bean injects至singleton StoreService bean的setter:
    • @Component
      public class StoreService {
        @Autowired
        public void setShoppingCart(ShoppingCart shoppingCart) { // shoppingCart會直到某個用戶進入系統,create session後才會存在
          this.shoppingCart = shoppingCart;
        }
      ... 
      }
      
    • 因此會希望能夠使用當下session的shoppingCart injects至setShoppingCat
    • 方法:
      • Spring並不會真的將實際的(session的) ShoppingCart injects至setShoppingCat,而是會injects一個shoppingCart的proxy
      • StoreServic而言,此proxy就是一個ShoppingCart
      • 當StoreServic需要使用ShoppingCart的method的時候,proxy會進行 lazily resolve(慵懶解譯),並且會將invoke delegate至實際的(session的) ShoppingCart
    • proxyMode
      • proxyMode=ScopedProxyMode.INTERFACE表示proxy要implements shoppingCart interface,並可以將invoke delegate至實際的(session的) ShoppingCart (只有當ShoppingCart是interface才能這樣做)
      • 如果ShoppingCart是一個class,則必須使用CGLib產生class的proxy:
        proxyMode=ScopedProxyMode.TARGET_CLASS 
  • 在XML中宣告scoped proxies
    • 要使用XML宣告,就不能使用@Scope及proxyMode
    • <bean> 的scope可以設定scope,proxy mode需要使用aop namespace
      • <bean id="cart"
              class="com.myapp.ShoppingCart"
              scope="session">
          <aop:scoped-proxy /> 
        </bean>
        
      • <aop:scoped-proxy>與@Scope annotation的proxyMode功能相同
      • <aop:scoped-proxy>default會使用CGLib建立的class proxy
      • 以下範例可以設置為interface的proxy
      • ..
        xmlns:aop="http://www.springframework.org/schema/aop" // 需要設定aop namesapce
        <bean
        id="cart" class="com.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false" /> </bean>
  • Runtime時injects value
    • 當討論DI 時,通常指的是一個bean ref injects到另一個bean property或constructor parameter中,也就是一個object和另一個object的關聯 
    • 當討論wire的的時候,指的是一個value injects到另一個bean property或constructor parameter中,這種時候可能會使用hard-code
      • @Bean
        public CompactDisc sgtPeppers() {
         return new BlankDisc(
          "Sgt. Pepper's Lonely Hearts Club Band", //hard-code
          "The Beatles");
        }
        
      • <bean id="sgtPeppers"
                      class="soundsystem.BlankDisc"
                      c:_title="Sgt. Pepper's Lonely Hearts Club Band"
                      c:_artist="The Beatles" /> //hard-code
        
    • 應盡力避免hard-code
    • Spring提供兩種runtime injects value的方式
      1. Property placeholder
      2. SpEL(Spring expression language)
  • Injects external values
    • 方式:@propertySource
    • @Configuration
      @PropertySource("classpath:/com/soundsystem/app.properties") 
      public class ExpressiveConfig {
       @Autowired
       Environment env;
       @Bean
       public BlankDisc disc() {
        return new BlankDisc(
         env.getProperty("disc.title"),
         env.getProperty("disc.artist"));
       }
      }
      
    • @PropertySource會load到Spring environment中,這樣的話就可以藉由getProperty讀取資訊
  • Spring environment
    • getProperty()是overloading method,共有四種
      1. String getProperty(String key)
      2. String getProperty(String key, String defaultValue) //key不存在時返回defaultValue
      3. T getProperty(String key, Class<T> type)
      4. T getProperty(String key, Class<T> type, T defaultValue) 
      • 3及4是為了轉型,如欲取得連接數量int時,不用在取得後再轉型,可以直接指定型態
      • int connectionCount =
            env.getProperty("db.connection.count", Integer.class, 30);
    • getRequiredProperty("disc.title") 可以讓沒有disc.title property的時候throw IllegalStateException(getProperty會回傳null)
    • containsProperty()確認property是否存在
    • getPropertyAsClass()可以將property解析為class type
      • Class<CompactDisc> cdClass =
        env.getPropertyAsClass("disc.class", CompactDisc.class);
        
    • String[] getActiveProfiles(): return active profiles
    • String[] getDefaultProfiles(): return default profiles
    • boolean acceptsProfiles(String... profiles): 如果profiles在active狀態則回傳true
    • 可以在bean create之前,使用acceptsProfiles()確保bean所需的profiles處於active狀態
  • 解析Property placeholder
    • 可以使用語法"${}"
    • 如XML內
    • <bean id="sgtPeppers"
            class="soundsystem.BlankDisc"
            c:_title="${disc.title}"
            c:_artist="${disc.artist}" />
      
    • 避免hard-code
    • 若使用component-scanning and autowiring,就沒有config file,這時候可以使用@Value如下
    • public BlankDisc(
            @Value("${disc.title}") String title,
            @Value("${disc.artist}") String artist) {
        this.title = title;
        this.artist = artist;
      }
      
    • 配置placeholder config: 
    • @Bean
      • PropertySourcesPlaceholderConfigurer:可基於Spring env及其property解析placeholder
      • @Bean
        public
        static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
          return new PropertySourcesPlaceholderConfigurer();
        }
        
    • XML:會協助生成PropertySourcesPlaceholderConfigurer class
      • <beans ..
         <context:property-placeholder />
        </beans>
    • 優點:解析外部property,讓value可以在runtime的時候生成,
    • 缺點:會需要使用Spring env和property resource。
    • 解決:使用Spring expression language
  • Spring expression language (SpEL)
    • 簡潔強大的方式將value wire到bean的property及constructor parameters中
    • runtime時取得value
    • 特性:
      • 使用bean ID引用bean
      • 使用object的method及取用其properties
      • 對value進行mathematical, relational, and logical operations 
      • Regular expression matching
      • Collections的操作
    • SpEL 範例
      • "#{...}" (placeholder是"$(...)"
      • T(將裡面的東西)視為Java中對應的type
      • #{T(System).currentTimeMillis()} // 取得當下計算的那刻時間
        
      • 引用其他bean或其他bean的property
      • #{sgtPeppers.artist}
        
      • 也可以使用systemProperties取用系統屬性
      • #{systemProperties['disc.title']}
        
      • 應用在constructor上
      • public BlankDisc(
              @Value("#{systemProperties['disc.title']}") String title,
              @Value("#{systemProperties['disc.artist']}") String artist) {
          this.title = title;
          this.artist = artist;
        }
        
      • 應用在XML上
      • <bean id="sgtPeppers"
              class="soundsystem.BlankDisc"
              c:_title="#{systemProperties['disc.title']}"
              c:_artist="#{systemProperties['disc.artist']}" />
        
    • Literal
      • #{3.14159}: floating
      • #{9.87E4}: 98,700
      • #{'Hello'}: String
      • #{false}: boolean
    • 引用bean
      • 透過ID引用其他的bean,假設是sgtPeppers bean
        • #{sgtPeppers}
          
      • 假設需要引用sgtPeppers的property (#{id.property})
        • #{sgtPeppers.artist}
          
      • 也能夠使用bean的method
        • #{artistSelector.selectArtist()}
          
      • method回傳的值可以繼續做invoke chain
        • #{artistSelector.selectArtist().toUpperCase()}
          
      • 如果要避免取到null,可以使用type-safe operator(?),能夠在執行問號右邊前確保左邊返回的值不是null。如果是null,就不會執行問號右邊的method
        • #{artistSelector.selectArtist()?.toUpperCase()}
          
    • 在expression中使用type
      • 使用T(),能夠使用static method及variable
      • 例如使用Java Math class
      • T(java.lang.Math)
        
      • 使用PI
      • T(java.lang.Math).PI
      • 使用static method
      • T(java.lang.Math).random()
    • SpEL operators
      • SpEL可以組合出複雜的expression
      • #{2 * T(java.lang.Math).PI * circle.radius}
        
      • 也可以使用eq表達相等 (比較運算gt, lt, le, eq, ge皆可使用)
      • #{counter.total eq 100}
        
      • 可以檢查是否為null,如下判斷disc.title是否為null,是的話expression結果就是Rattle and Hum(此種表達式稱為Elvis operator)
      • #{disc.title ?: 'Rattle and Hum'}
        
    • matches (正規表達式)
      • 範例:判斷是否符合有效mail address
      • #{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
        
    • Collections
      • 隨機取得id=jukebox的bean中的songs property的一首歌,可使用[]
      • #{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title}
        
      • 從corrections或array中取得item
      • #{'This is a test'[3]} // return 's'
        
      • 取得歌曲內符合'Aerosmith'的歌曲,使用(.?[])進行過濾,得到collection的subCollection,也就是在[]內在寫一個expression language
      • #{jukebox.songs.?[artist eq 'Aerosmith']}
        
      • 第一個符合(.^[]),最後一個符合(.$[])
      • #{jukebox.songs.^[artist eq 'Aerosmith']}
      • 使用(.![])可以從子合中選特定的property放入。比如下例,將所有歌曲名稱的title property放入新的String type collection中
      • #{jukebox.songs.![title]}
        
      • 也可以組合一起用
      • #{jukebox.songs.?[artist eq 'Aerosmith'].![title]}
        
    • 注意:不要把SpEL寫得太過複雜,寫得越複雜,對它的測試也就越多,盡可能保持簡潔

沒有留言:

張貼留言