Core Spring

Spring in Action》读书笔记和总结。Spring官网: spring.io

Spring的底层功能依赖于两个核心特性,依赖注入(dependency injection, DI)和面向切面编程(aspect-oriented programming, AOP)。Spring简化了Java开发,提供了轻量级的编程模型,增强了POJO(plain old Java object)的功能。DI和AOP都是为了实现接口和功能的松耦合(Loose Coupling),并且在实现上最大化的采用最小侵入性编程。


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

DI

实现接口上的松耦合。为了关联各接口之间的调用和依赖,Spring采用装配bean方式。建立应用接口间依赖关系的行为称为装配(Wiring)。

Spring应用上下文(Application Context)负责对象的创建和组装。

Spring提供三种主要的装配注入机制:

  1. 隐式的bean自动装配机制;
  2. 显示Java配置;
  3. 显示XML配置。

自动化装配bean

先定义需要的组件component,其次开启组件扫描(Component scanning),最后调用其他组件。Spring会自动发现应用上下文中创建的bean。

定义组件

@Component("beanName")  //beanName不填写,默认为首字母小写的类名
public class Impl implements Interface {
}

开启组件扫描

@Configuration
@ComponentScan
public class AutoConfig { 
}

默认以配置类所在的包作为基础包(base package)扫描组件。@ComponentScan(basePackages="pkgName")一个包、@ComponentScan(basePackages={"pkgName1", "pkgName2"})多个包、@ComponentScan(basePackageClasses={Interface1.class, Impl2.class})类或接口所在的包作为组件扫描的基础包。

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

调用其他组件

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

Java Config

添加配置并声明各接口调用依赖关系即可。

@Configuration
public class EatConfig {

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

}

XML Config

构造器注入

可以使用全称<constructor-arg />c-标签

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

作为一个通用规则,对强依赖使用构造器注入,对弱依赖使用属性注入。

属性注入

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

导入和混合配置

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

应用程序在不同环境的迁移,如数据库配置、加密算法以及与外部系统集成是否Mock是跨环境部署时会发生变化的几个典型例子。
如果在XML配置文件中配置(Maven的profiles),在构建阶段确定将配置编译部署,问题在于为每种环境重新构建应用,而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

有多种方式设置这两个属性,spring.profiles.active优先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应用中,设置spring.profiles.default的web.xml文件

<web-app ...>
    <!-- 为上下文设置默认的profile -->
    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </context-param>
    <!-- 为Servlet设置默认的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创建后或环境变量设置后才会创建这个bean。Spring4引入了@Conditional注解。

@Configuration
public class MagicConfig {

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

设置给@Conditional的类需实现Condition接口,它会通过该接口进行对比。

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

matches()返回true,会创建带有@Conditional注解的类。否则反之。
PS: @Profile本身也使用了@Conditional注解,并引用ProfileCondition作为Condition实现。可查看Spring4以上源码。

处理自动装配歧义性

场景: 一个接口多个实现。

  1. 定义限定符
@Component    //也可以是所有使用了@Component的注解,如: @Service、@Controller、@Ropository等
@Qualifier("one")   //该注解可省略,默认bean ID为首字母为小写的实现类名字。
public class One implements Number {
    ...
}
  1. 使用限定符。@Qualifier搭配@Autowired,@Autowired默认按类型装配
@Autowired
@Qualifier("one")
private Number num;

或者直接使用J2EE自带的@Resource,默认按名称进行装配,减少了与spring的耦合(推荐)。

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

bean的作用域

默认情况下,Spring Application Context中所有bean都是以单例(singleton)创建的。也就是说,不管特定的bean被注入到其他bean多少次,每次注入的都是同一个实例。大多数情况下,单例bean是很理想的状态。但有时候所用类是mutable,重用是不安全的。
Spring定义了四种作用域:

  • 单例(singleton):在整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Request):在Web应用中,为每次请求创建一个bean实例。

声明bean为原型作用域

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

声明bean为会话和请求作用域

在Web应用中,以购物车bean为例,单例和原型作用域就不适用,会话作用域是最合适的,因为它与特定用户关联性最大。

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

在每个用户购物完成后会调用保存订单service,也就是会将会话级别的 ShoppingCart bean注入到单例级别的 StoreService bean中。proxyMode属性解决了将会话或请求作用域的bean注入到单例bean的问题。

作用域代理能够延迟注入请求和会话作用域的bean

基于接口代理:proxyMode=ScopedProxyMode.INTERFACES or <aop:scoped-proxy proxy-target-class="false" />
基于实现类代理: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>

使用解析参数:

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

Spring表达式语言(The Spring Expression Language, SpEL)

SpEL表达式要放在"#{ ... }"之中。

  • 引用bean、属性和方法。例如:#{sgtPeppers}#{sgtPeppers.artist}#{artistSelector.selectArtist()}#{artistSelector.selectArtist().toUpperCase()}
  • 访问Java类。'T()'运算符结果是一个class对象,能够访问目标类型的静态方法和常量。#{T(System).currentTimeMillis()}T(java.lang.Math).PIT(java.lang.Math).random()
  • 对值进行算术、关系和逻辑运算。#{T(java.lang.Math).PI * circle.radius ^ 2}#{disc.title + ' by ' + disc.artist}#{scoreboard.score > 1000 ? "Winner!" : "Loser"}
  • 匹配正则表达式(Regular Expression, regex)。matches运算符对String类型的文本(左边参数)应用正则(右边参数)。#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
  • 计算集合。#{jukebox.songs[4].title};查询运算符".?[]"用来对集合进行过滤得到集合子集,#{jukebox.songs.?[artist eq 'Aerosmith']};查询第一个匹配项".^[]"和查询最后一个匹配项".$[]";投影运算符".![]",如将title属性投影到一个新的String类型集合中#{jukebox.songs.![title]}

PS: 尽可能让表达式保持简洁,不要让表达式太智能复杂。

AOP

实现功能上的松耦合。把系统核心业务逻辑组件和额外功能如日志、事务管理和安全这样的服务组件分离开来。

<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)负责创建、装配、配置并管理对象的整个生命周期,从生存到死亡。Spring容器分为两种类型: BeanFactory(bean工厂)是最简单的容器,提供基本的DI支持;Application Context(应用上下文)基于BeanFactory构建,提供应用框架级别的服务。(推荐使用)

通知(Advice)

通知定义了切面是什么以及何时使用。Spring切面可以应用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.
  • 环绕通知-@Around: The advice wraps the advised method, providing some functionality before and after the advised method is invoked.

切点(Pointcut)

切点定义在何处执行动作。Spring AOP所支持的AspectJ切点指示器: args()@args()execution()this()target()@target()within()@within()@annotation。只有execution指示器是实际执行匹配的,其他都是用来限制匹配的,所以execution指示器是编写切点定义时最主要的指示器。

使用AspectJ切点表达式定义切点
使用within()限制切点范围

bean()指示器使用bean ID或name作为参数来限制切点只匹配特定的bean。execution(* concert.Performance.perform()) and bean('woodstock')

基本流程

  1. 定义切面
JavaAdvice

XML Config:

XMLAdvice
  1. 启用AspectJ注解的自动代理
JavaConfig
XMLConfig

创建环绕通知

JavaAround
XMLAround

通知中增加参数

JavaArgument
XMLArgument

切点表达式中的args(trackNumber)限定符表明传递给 playTrack() 方法的int类型参数也会传递到通知中去,参数的名称trackNumber也与切点方法签名中的参数相匹配,这样就完成了从命名切点到通知方法的参数转移。

通过注解引入新功能

不用直接修改对象或类的定义就能够为对象或类增加新的方法。一种情况是设计上在原接口上增加通用方法对所有的实现并不适用,一种情况是使用第三方实现没有源码的时候。借助于AOP的引入功能,不必在设计上妥协或者侵入性地改变现有的实现。

代理拦截调用并委托给实现该方法的其他对象
@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注解将新接口引入到现有的bean中。value属性指定引入到哪个接口bean上,加号'+'表示该对象的子类型,而不是其本身;defaultImpl属性指定了引入的功能;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容