Core Spring

Spring in Action》讀書筆記和總結(jié)。Spring官網(wǎng): spring.io

Spring的底層功能依賴于兩個核心特性,依賴注入(dependency injection, DI)和面向切面編程(aspect-oriented programming, AOP)。Spring簡化了Java開發(fā),提供了輕量級的編程模型,增強了POJO(plain old Java object)的功能。DI和AOP都是為了實現(xiàn)接口和功能的松耦合(Loose Coupling),并且在實現(xiàn)上最大化的采用最小侵入性編程。


The Spring Framework is made up of six well-defined module categories

DI

實現(xiàn)接口上的松耦合。為了關(guān)聯(lián)各接口之間的調(diào)用和依賴,Spring采用裝配bean方式。建立應(yīng)用接口間依賴關(guān)系的行為稱為裝配(Wiring)。

Spring應(yīng)用上下文(Application Context)負(fù)責(zé)對象的創(chuàng)建和組裝。

Spring提供三種主要的裝配注入機制:

  1. 隱式的bean自動裝配機制;
  2. 顯示Java配置;
  3. 顯示XML配置。

自動化裝配bean

先定義需要的組件component,其次開啟組件掃描(Component scanning),最后調(diào)用其他組件。Spring會自動發(fā)現(xiàn)應(yīng)用上下文中創(chuàng)建的bean。

定義組件

@Component("beanName")  //beanName不填寫,默認(rèn)為首字母小寫的類名
public class Impl implements Interface {
}

開啟組件掃描

@Configuration
@ComponentScan
public class AutoConfig { 
}

默認(rèn)以配置類所在的包作為基礎(chǔ)包(base package)掃描組件。@ComponentScan(basePackages="pkgName")一個包、@ComponentScan(basePackages={"pkgName1", "pkgName2"})多個包、@ComponentScan(basePackageClasses={Interface1.class, Impl2.class})類或接口所在的包作為組件掃描的基礎(chǔ)包。

<context:component-scan base-package="pkg" />

調(diào)用其他組件

@Component
public class Impl implements Interface {
    @Autowired
    private InvokedClass ref;
}

Java Config

添加配置并聲明各接口調(diào)用依賴關(guān)系即可。

@Configuration
public class EatConfig {

  @Bean
  public Fruit fruit() {
    return new Apple(System.out);
  }
  
  @Bean
  public Person person() {
    return new Man(fruit());
  }

}

XML Config

構(gòu)造器注入

可以使用全稱<constructor-arg />c-標(biāo)簽

<bean id="person" class="eat.Person">
    <constructor-arg ref="fruit" />    <!-- bean引用注入 -->
</bean>

<bean id="fruit" class="eat.Fruit">
    <constructor-arg value="#{T(System).out}" />    <!-- 字面量注入 -->
</bean>

<bean id="list" class="eat.Collection">
    <constructor-arg>
      <list>    <!-- 集合注入 -->
        <ref bean="one" />
        <ref bean="two" />
        <ref bean="three" />
      </list>
    </constructor-arg>
</bean>

<bean id="list" class="eat.Collection">
    <constructor-arg>
      <list>    <!-- 集合注入 -->
        <value>one</value>
        <value>two</value>
        <value>three</value>
      </list>
    </constructor-arg>
</bean>

作為一個通用規(guī)則,對強依賴使用構(gòu)造器注入,對弱依賴使用屬性注入。

屬性注入

<bean id="person" class="eat.Person">
    <property name="fruit" ref="fruit" />   <!-- 引用注入 -->
</bean>

<bean id="fruit" class="eat.Fruit">
  <property name="apple" value="I eating an apple" />   <!-- 字面量注入 -->
  <property name="num">
    <list>  <!-- 集合注入 -->
      <value>one</value>
      <value>two</value>
      <value>three</value>
    </list>
  </property>
</bean>

導(dǎo)入和混合配置

Java Config

@Configuration
@Import({OneConfig.class, TwoConfig.class})
@ImportResource("classpath:three-config.xml")
public class RootConfig {

}

XML Config

<bean class="package.OneConfig" />
<bean resource="three-config.xml" />

高級裝配

Profile

應(yīng)用程序在不同環(huán)境的遷移,如數(shù)據(jù)庫配置、加密算法以及與外部系統(tǒng)集成是否Mock是跨環(huán)境部署時會發(fā)生變化的幾個典型例子。
如果在XML配置文件中配置(Maven的profiles),在構(gòu)建階段確定將配置編譯部署,問題在于為每種環(huán)境重新構(gòu)建應(yīng)用,而Spring的profile是在運行時確定配置源。

定義profile

@Configuration
public class DataSourceConfig {
  
  @Bean(destroyMethod = "shutdown")
  @Profile("dev")
  public DataSource embeddedDataSource() {

  }

  @Bean
  @Profile("prod")
  public DataSource jndiDataSource() {

  }
}
<beans profile="dev">
  <jdbc:embedded-database id="dataSource" type="H2">
    <jdbc:script location="classpath:schema.sql" />
    <jdbc:script location="classpath:test-data.sql" />
  </jdbc:embedded-database>
</beans>

<beans profile="prod">
  <jee:jndi-lookup id="dataSource"
    lazy-init="true"
    jndi-name="jdbc/myDatabase"
    resource-ref="true"
    proxy-interface="javax.sql.DataSource" />
</beans>

激活profile

有多種方式設(shè)置這兩個屬性,spring.profiles.active優(yōu)先spring.profiles.default。

  • As initialization parameters on DispatcherServlet
  • As context parameters of a web application
  • As JNDI entries
  • As environment variables
  • As JVM system properties
  • Using the @ActiveProfiles annotation on an integration test class

例如在Web應(yīng)用中,設(shè)置spring.profiles.default的web.xml文件

<web-app ...>
    <!-- 為上下文設(shè)置默認(rèn)的profile -->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
    <!-- 為Servlet設(shè)置默認(rèn)的profile -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>spring.profiles.default</param-name>
            <param-value>dev</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
</web-app>

Conditional beans

如果希望某個特定的bean創(chuàng)建后或環(huán)境變量設(shè)置后才會創(chuàng)建這個bean。Spring4引入了@Conditional注解。

@Configuration
public class MagicConfig {

  @Bean
  @Conditional(MagicExistsCondition.class)
  public MagicBean magicBean() {
    return new MagicBean();
  }
  
}

設(shè)置給@Conditional的類需實現(xiàn)Condition接口,它會通過該接口進(jìn)行對比。

public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches()返回true,會創(chuàng)建帶有@Conditional注解的類。否則反之。
PS: @Profile本身也使用了@Conditional注解,并引用ProfileCondition作為Condition實現(xiàn)??刹榭碨pring4以上源碼。

處理自動裝配歧義性

場景: 一個接口多個實現(xiàn)。

  1. 定義限定符
@Component    //也可以是所有使用了@Component的注解,如: @Service、@Controller、@Ropository等
@Qualifier("one")   //該注解可省略,默認(rèn)bean ID為首字母為小寫的實現(xiàn)類名字。
public class One implements Number {
    ...
}
  1. 使用限定符。@Qualifier搭配@Autowired,@Autowired默認(rèn)按類型裝配
@Autowired
@Qualifier("one")
private Number num;

或者直接使用J2EE自帶的@Resource,默認(rèn)按名稱進(jìn)行裝配,減少了與spring的耦合(推薦)。

@Resource(name = "one")
private Number num;

bean的作用域

默認(rèn)情況下,Spring Application Context中所有bean都是以單例(singleton)創(chuàng)建的。也就是說,不管特定的bean被注入到其他bean多少次,每次注入的都是同一個實例。大多數(shù)情況下,單例bean是很理想的狀態(tài)。但有時候所用類是mutable,重用是不安全的。
Spring定義了四種作用域:

  • 單例(singleton):在整個應(yīng)用中,只創(chuàng)建bean的一個實例。
  • 原型(Prototype):每次注入或通過Spring應(yīng)用上下文獲取的時候,都會創(chuàng)建一個新的bean實例。
  • 會話(Session):在Web應(yīng)用中,為每個會話創(chuàng)建一個bean實例。
  • 請求(Request):在Web應(yīng)用中,為每次請求創(chuàng)建一個bean實例。

聲明bean為原型作用域

JavaConfig@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);XMLConfigscope="prototype"。

聲明bean為會話和請求作用域

在Web應(yīng)用中,以購物車bean為例,單例和原型作用域就不適用,會話作用域是最合適的,因為它與特定用戶關(guān)聯(lián)性最大。

@Component
@Scope(
    value=WebApplicationContext.SCOPE_SESSION,
    proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }

在每個用戶購物完成后會調(diào)用保存訂單service,也就是會將會話級別的 ShoppingCart bean注入到單例級別的 StoreService bean中。proxyMode屬性解決了將會話或請求作用域的bean注入到單例bean的問題。

作用域代理能夠延遲注入請求和會話作用域的bean

基于接口代理:proxyMode=ScopedProxyMode.INTERFACES or <aop:scoped-proxy proxy-target-class="false" />
基于實現(xiàn)類代理:proxyMode=ScopedProxyMode.TARGET_CLASS or <aop:scoped-proxy />

運行時值注入

避免將值硬編碼在配置類中,使其在運行時確定。

聲明屬性源并通過Spring的Environment來檢索屬性

@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")    //聲明屬性源
public class EnvironmentConfig {

  @Autowired
  Environment env;
  
  @Bean
  public BlankDisc blankDisc() {
    return new BlankDisc(
        env.getProperty("disc.title"),
        env.getProperty("disc.artist"));    //檢索屬性值
  }
  
}

屬性占位符(Property placeholders)

為了使用占位符,需要配置一個 PropertySourcesPlaceholderConfigurer bean,它能夠基于Spring Environment及其屬性源來解析占位符。

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
  return new PropertySourcesPlaceholderConfigurer();
}
<beans>
    <context:property-placeholder />    <!-- 自動生成PropertySourcesPlaceholderConfigurer -->
</beans>

使用解析參數(shù):

public BlankDisc(
      @Value("${disc.title}") String title,
      @Value("${disc.artist}") String artist) {
  this.title = title;
  this.artist = artist;
}

Spring表達(dá)式語言(The Spring Expression Language, SpEL)

SpEL表達(dá)式要放在"#{ ... }"之中。

  • 引用bean、屬性和方法。例如:#{sgtPeppers}、#{sgtPeppers.artist}、#{artistSelector.selectArtist()}、#{artistSelector.selectArtist().toUpperCase()}
  • 訪問Java類。'T()'運算符結(jié)果是一個class對象,能夠訪問目標(biāo)類型的靜態(tài)方法和常量。#{T(System).currentTimeMillis()}、T(java.lang.Math).PIT(java.lang.Math).random()
  • 對值進(jìn)行算術(shù)、關(guān)系和邏輯運算。#{T(java.lang.Math).PI * circle.radius ^ 2}、#{disc.title + ' by ' + disc.artist}、#{scoreboard.score > 1000 ? "Winner!" : "Loser"}
  • 匹配正則表達(dá)式(Regular Expression, regex)。matches運算符對String類型的文本(左邊參數(shù))應(yīng)用正則(右邊參數(shù))。#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
  • 計算集合。#{jukebox.songs[4].title};查詢運算符".?[]"用來對集合進(jìn)行過濾得到集合子集,#{jukebox.songs.?[artist eq 'Aerosmith']};查詢第一個匹配項".^[]"和查詢最后一個匹配項".$[]";投影運算符".![]",如將title屬性投影到一個新的String類型集合中#{jukebox.songs.![title]}

PS: 盡可能讓表達(dá)式保持簡潔,不要讓表達(dá)式太智能復(fù)雜。

AOP

實現(xiàn)功能上的松耦合。把系統(tǒng)核心業(yè)務(wù)邏輯組件和額外功能如日志、事務(wù)管理和安全這樣的服務(wù)組件分離開來。

<aop:config>
  <aop:aspect ref="asp">
    <aop:pointcut id="pc"
        expression="execution(* *.method(..))"/>
      
    <aop:before pointcut-ref="pc" 
        method="doBeforePc"/>

    <aop:after pointcut-ref="pc" 
        method="doAfterPc"/>
  </aop:aspect>
</aop:config>

Spring容器(container)負(fù)責(zé)創(chuàng)建、裝配、配置并管理對象的整個生命周期,從生存到死亡。Spring容器分為兩種類型: BeanFactory(bean工廠)是最簡單的容器,提供基本的DI支持;Application Context(應(yīng)用上下文)基于BeanFactory構(gòu)建,提供應(yīng)用框架級別的服務(wù)。(推薦使用)

通知(Advice)

通知定義了切面是什么以及何時使用。Spring切面可以應(yīng)用5種類型的通知,并使用AspectJ注解來聲明通知方法:

  • 前置通知-@Before: The advice functionality takes place before the advised method is invoked.
  • 后置通知-@After: The advice functionality takes place after the advised method completes, regardless of the outcome.
  • 返回通知-@AfterReturning: The advice functionality takes place after the advised method successfully completes.
  • 異常通知-@AfterThrowing: The advice functionality takes place after the advised method throws an exception.
  • 環(huán)繞通知-@Around: The advice wraps the advised method, providing some functionality before and after the advised method is invoked.

切點(Pointcut)

切點定義在何處執(zhí)行動作。Spring AOP所支持的AspectJ切點指示器: args()、@args()、execution()、this()、target()、@target()、within()@within()、@annotation。只有execution指示器是實際執(zhí)行匹配的,其他都是用來限制匹配的,所以execution指示器是編寫切點定義時最主要的指示器。

使用AspectJ切點表達(dá)式定義切點
使用within()限制切點范圍

bean()指示器使用bean ID或name作為參數(shù)來限制切點只匹配特定的bean。execution(* concert.Performance.perform()) and bean('woodstock')。

基本流程

  1. 定義切面
JavaAdvice

XML Config:

XMLAdvice
  1. 啟用AspectJ注解的自動代理
JavaConfig
XMLConfig

創(chuàng)建環(huán)繞通知

JavaAround
XMLAround

通知中增加參數(shù)

JavaArgument
XMLArgument

切點表達(dá)式中的args(trackNumber)限定符表明傳遞給 playTrack() 方法的int類型參數(shù)也會傳遞到通知中去,參數(shù)的名稱trackNumber也與切點方法簽名中的參數(shù)相匹配,這樣就完成了從命名切點到通知方法的參數(shù)轉(zhuǎn)移。

通過注解引入新功能

不用直接修改對象或類的定義就能夠為對象或類增加新的方法。一種情況是設(shè)計上在原接口上增加通用方法對所有的實現(xiàn)并不適用,一種情況是使用第三方實現(xiàn)沒有源碼的時候。借助于AOP的引入功能,不必在設(shè)計上妥協(xié)或者侵入性地改變現(xiàn)有的實現(xiàn)。

代理攔截調(diào)用并委托給實現(xiàn)該方法的其他對象
@Aspect
public class EncoreableIntroducer {

  @DeclareParents(value="concert.Performance+",
                  defaultImpl=DefaultEncoreable.class)
  public static Encoreable encoreable;

}
<aop:aspect>
  <aop:declare-parents
    types-matching="concert.Performance+"
    implement-interface="concert.Encoreable"
    default-impl="concert.DefaultEncoreable"
    />
</aop:aspect>

通過@DeclareParents注解將新接口引入到現(xiàn)有的bean中。value屬性指定引入到哪個接口bean上,加號'+'表示該對象的子類型,而不是其本身;defaultImpl屬性指定了引入的功能;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容