第一部分:Spring的核心
1、第一章:Spring之旅
1.1、简化Java开发
创建Spring的主要目的是用来替代更加重量级的企业级Java技术,尤其是EJB。相对EJB来说,Spring提供了更加轻量级和简单的编程模型。
它增强了简单老式Java对象(Plain Old Java Object,POJO),使其具备了之前只有EJB和其他企业级Java规范才具有的功能
为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模板减少样式代码
1.1.1、Spring赋予POJO魔力的方式之一就是通过DI来装配它们。帮助应用对象彼此之间保持松散耦合
1.1.2、依赖注入
任何一个有实际意义的应用都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻辑。
按照传统的做饭,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将导致高度耦合和难以测试的代码
耦合具有两面性(two-headed beast)。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的Bug特性
另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。
总而言之,耦合是必须的,但应当被小心谨慎地管理
//DamselRescuingKnight只能执行RescueDamselQuest探险任务 package com.springinaction.knights; public class DameselRecuingKnight implements Knight { private RescueDamselQuest quest; public DamselRescuingKnight() {
// 与RescueDamselQuest 紧耦合
this.request = new RescueDamselQuest();
}
public void embarkOnQuest() {
quest.embark();
}
}
DamselRescuingKnight与RescueDamselQuest耦合到了一起,极大的限制了这个骑士执行探险的能力。
如果一个少女需要救援,这个骑士能够招之即来。但如果一条恶龙需要杀掉,或者一个圆桌需要被滚起来,那么这个骑士就爱莫能助了
更糟糕的是,为DamselRescuingKnight编写单元测试将出奇的困难。
在这样的一个测试中,你必须保证当骑士的embarkOnQuest()方法被调用的时候,探险的embark()方法也要被调用。但是没有一个简明的方式能够实现这一点。
DamselRescuingKnight将无法测试
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。
对象无需自行创建或者管理他们的依赖关系
依赖注入会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖
//BraveKnight足够灵活可以接受任何赋予它的探险任务
public class BraveKnight implements Knight { private Quest quest; public BraveKnight(Quest quest) { //Quest被注入进来
this.quest = quest;
} public void embarkOnQuest() {
quest.embark();
}
}
BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造参数传入。
这是依赖注入的方式之一,即构造器注入(constructor injection)
更主要的是,传入的是探险类型Quest,也就是所有探险任务都必须实现的一个接口
这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。
DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表面依赖关系,那么这种依赖就能在对象本身毫不知情的情况下,用不同的具体实现进行替换
对依赖进行替换的一个最常用方法就是在测试的时候使用mock。我们无法充分测试DamselRescuingKniht,因为它是紧耦合的;
但是可以轻松地测试BraveKnight,只需要给它一个Quest的mock实现即可
//为了测试BraveKnight,需要注入一个mock Quest
pakeage com.springinaction.knights;
import static org.mockito.Mockito.*;
import org.junit.Test; public class BraveKnightTest {
@Test
public void knightShouldEmbarkOnQuest() {
Quest mockQuest = mock(Quest.class); //创建mock Quest
BraveKnight knight = new BraveKnight(mockQuest); //注入mock Quest
knight.embarkOnQuest();
verify(mockQuest, times(1)).embark();
}
}
现在BraveKnight类可以接受你传递给它的任意一种Quest的实现,但是该如何把特定的Quest实现传递给它呢?
//SlayDragonQueset是要注入到BraveKnight中的Quest实现
import java.io.PrintStream; public class SlayDragonQuest implements Quest {
private PrintStream stream;
public SlayDragonQuest(PrintStream stream) {
this.stream = stream;
} public void embark() {
stream.println("Embarking on quest to slay the dragon!");
}
}
上面的代码中,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢?
创建应用组件之间协作的行为通常称为装配(wiring)。下面是XML装配bean的方式:
//使用Spring将SlayDragonQuest注入到BraveKnight中 <bean id="knight" class="com.springinaction.knights.BraveKnight">
<constructor-arg ref="quest" /> /*注入Quest bean*/
<bean> /*创建SlayDragonQuest*/
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
<constructor-arg value="#{T(System).out}" />
</bean>
在这里,BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight来讲,它在构造时传入了对SlayDragonQuest bean的引用,将其作为构造器参数
同时SlayDragonQuest bean的声明使用了Spring表达式语言(Spring Expression Language),将System.out(这是一个PrintStream)传入到了SlayDragonQuest的构造器中
Spring提供了基于Java的配置,可作为XML的替代方案
@Configuration
public class KnightConfig {
@Bean
public Knight knight() {
return new BraveKnight(quest());
} @Bean
public Quest quest() {
return new SlayDragonQuest(System.out);
}
}
关于@Configuration,可参考这篇文章:https://www.cnblogs.com/duanxz/p/7493276.html
现在已经声明了BraveKnigt和Quest的关系,接下来只需要装载XML配置文件,并把应用启动起来
Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。
Spring应用上下文全权负责对象的创建和组装。Spring自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置
因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext作为应用上下文。
//KnightMain.java加载包含Knight的Spring上下文
import org.springframework.context.support.ClassPathXmlApplicationContext; public class KnightMain {
public static void main(String[] args) throws Exception {
//加载Spring上下文
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");
//获取knight bean
Knight knight = context.getBean(Knight.class);
knight.embarkOnQuest();
context.close();
}
}
main()方法基于knight.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean
得到Knight对象的引用后,只需要简单调用embarkOnQuest()方法即可
这个类完全不知道我们的英雄骑士接受哪种探险任务,而且完全没有意识到这是由BraveKnight来执行的。只有knights.xml文件知道哪个骑士执行哪种任务
1.1.3、应用切面
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件
AOP实现关注点分离。如日志、事务管理和安全这样的系统服务,通常被称为横切关注点,因为它们会跨越系统的多个组件
AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。这样,这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解涉及系统服务带来的复杂性。总之,AOP能够确保POJO的简单性
如下图所示,我们可以把切面想象为覆盖在很多组件上的一个外壳。使用AOP可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,你的核心应用甚至根本不知道它们的存在
//使用吟游诗人这个服务类来记载骑士的所有事迹
import java.io.PrintStream; public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() {
stream.println("Fa la la,the knight is so brave!");
}
public void singAfterQuest() {
stream.println("Tee hee hee, the brave knight " +
"did embark on a quest!");
}
}
//BraveKnight必须要调用Minstrel的方法
public class BraveKnight implements Knight {
private Quest quest;
private Minstrel minstrel;
public BraveKnight(Quest quest, Minstrel minstrel) {
this.quest = quest;
this.minstrel = minstrel;
}
public void embarkOnQuest() throws QuestException {
//Knight应该管理它的Minstrel吗?
minstrel.singBeforeQuest();
quest.embark();
minstrel.singAfterQuest();
}
}
将Minstrel抽象为一个切面,所需要做的事情就是在一个Spring配置文件中声明它
/*声明Minstrel bean*/
<bean id="minstrel" class="com.springinaction.knights.Minstrel">
<constructor-arg value="#{T(System).out}" />
</bean> <aop:config>
<aop:aspect ref="minstrel">
<aop:pointcut id="embark"
expression="execution(* *.embarkOnQuest(...))" /> //定义切点
<aop:before pointcut-ref="embark" method="singBeforeQuest" /> //声明前置通知
<aop:after pointcut-ref="embark" method="singAfterQuest" /> //声明后置通知 </aop:aspect>
</aop:config>
1.1.4、使用模板消除样板式代码
许多JavaAPI,如JDBC,会涉及编写大量的样板式代码:如首先需要创建数据连接,然后再创建一个语句对象,然后才能进行查询
JMS、JNDI和使用REST服务通常也涉及大量的重复代码
模板能够让你的代码关注于自身的职责
Spring的JdbcTemplate,参考:https://www.cnblogs.com/caoyc/p/5630622.html
Spring对数据库的操作在jdbc上面做了深层次的封装,使用spring的注入功能,可以把DataSource注册到JdbcTemplate之中。
主要提供以下五类方法:
execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
query方法及queryForXXX方法:用于执行查询相关语句;
call方法:用于执行存储过程、函数相关语句。
1.2、容纳你的Bean
在基于Spring的应用中,你的应用对象生存于Spring容器(container)中。
Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡(在这里可能就是new到finalize())
容器是Spring框架的核心。容器并不是只有一个。Spring自带来多个容器实现,可以归为两种不同类型:
bean工厂(由org.springframework.beans.factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持
应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者
1.2.1、使用应用上下文
- AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文
- AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文
- ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源(在所有的类路径——包含JAR文件下查找xml)
- FileSystemXmlApplicationConext:从文件系统下的一个或多个XML配置文件中加载上下文定义
- XMLWebApplicationContext:从web应用下的一个或多个XML配置文件中加载上下文定义
无论从文件系统还是从类路径下装载应用上下文,将bean加载到bean工厂的过程都是类似的
ApplicationContext context = new FileSystemXmlApplicationContext("c:/knight.xml");
ApplicationContext context = new AnnotationConfigApplicationContext(com.springinaction.knights.config.KnightConfig.class);
在应用上下文准备就绪之后,我们就可以调用上下文的getBean()方法从Spring容器中获取bean
1.2.2 bean的生命周期
Bean的生命周期,参考:https://www.cnblogs.com/xujian2014/p/5049483.html
1.3.1、Spring模块
Spring Portfolio几乎为每一个领域的Java开发都提供了Spring编程模型,如:
Spring Web Flow:为基于流程的会话式Web应用(可以想一下购物车或者向导功能)提供了支持
Spring Web Service
Spring Security:利用AOP,Spring Security为Spring应用提供了声明式安全机制
Spring Integration:提供了多种通用应用集成模式的Spring声明式风格实现
Spring Batch:需要对数据进行大量操作时,如需要开发一个批处理应用。
Spring Data:JPA
Spring Social:
Spring Boot
2、第二章:装配Bean
- 声明bean
- 构造器注入和Setter方法注入
- 装配bean
- 控制bean的创建和销毁
在Spring中,对象无需自己查找或创建与其相关联的其他对象。容器负责把需要相互协作的对象引用赋予给各个对象
创建应用对象之间协作关系的行为称为装配(wiring),这也是依赖注入(DI)的本质
什么是bean:https://blog.csdn.net/weixin_42594736/article/details/81149379
https://www.cnblogs.com/cainiaotuzi/p/7994650.html
配置Spring容器最常见的三种方法:
2.1、 Spring配置的可选方案——尽可能的使用自动装配机制,显式配置越少越好
- 在XML中进行显式配置
- 在Java中进行显式配置
- 隐式的bean发现机制和自动装配
2.2、Spring从两个角度来实现自动化装配bean:
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean
- 自动装配(autowiring):Spring自动满足bean之间的依赖
CD为我们阐述DI如何运行提供了一个很好的样例——你不将CD插入(注入)到CD播放器中,那么播放器其实是没有太大用处的。CD播放器依赖于CD才能完成它的使命
//CompactDisc接口在Java中定义了CD的概念
package soundsystem;
public interface CompactDisc {
void play();
}
CompactDisc作为接口,它定义了CD播放器对一盘CD所能进行的操作,它将CD播放器的任意实现与CD本身的耦合降低到了最小的程度
我们还需要一个CompactDisc的实现
//带有@Component注解的CompactDisc实现类SgtPeppers
package soundsystem;
import org.springframework.stereotype.Component;
@Component
public class SgtPeppers implements CompactDisc {
private String title = "Sgt.Pepper's Lonely Hearts Club Band";
private String artist = "The Beatles"; public void play() {
System.out.println("Playing " + title + " by " + artist);
}
}
SgtPeppers类上使用了@Component注解,这个注解表明该类会作为组件类,并告知Spring要为这个类创建bean。
组件扫描默认是不启用的。还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean
//@Component注解启用了组件扫描
package soundsystem;
import org.springframework.context.annotation.componentScan;
import org.springframework.context.annotation.Con;
@Configuration
@ComponentScan
public class CDPlayerConfig {
}
CDPlayerConfig类并没有显式声明任何bean,只不过它使用了@ComponentScan注解,这个注解能够在Spring中启用组件扫描
如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。
因为CDPlayerConfig类位于soundsystem包中,因此Spring将会扫描这个包以及这个包下的所有子包,查找带有@Component注解的类
这样,就能发现CompactDisc,并且会在Spring中自动为其创建一个bean
也可以用XML来启用组件扫描,可以使用Spring context命名空间的<context:component-scan>元素
<context:component-scan base-package="soundsystem" />
下面,我们创建一个简单的JUnit测试,它会创建Spring上下文,并判断CompactDisc是不是真的创建出来了
package soundsystem;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
//使用SpringJUnit4ClassRunner以便在测试开始的时候自动创建Spring的应用上下文
//注解ContextConfiguration会告诉它需要在CDPlayerConfig中加载配置
//因为CDPlayerConfig类中包含了@ComponentScan,因此最终的应用上下文应该包含CompactDiscbean
//为了证明这一点,在测试代码中有一个CompactDisc类型的属性,并且这个属性带有@Autowired注解,以便于将CompactDiscbean注入到测试代码中
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {
@Autowired
private CompactDisc cd;
@Test
public void cdShouldNotBeNull() {
assertNotNull(cd);
}
}
只添加一行@ComponentScan注解就能自动创建无数个bean
2.2.2、为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID。默认会根据类名为其指定一个ID,如上面例子中SgtPeppersbean的ID为sgtPeppers,也就是将类名的第一个字母变小写
//设定bean的ID
@Component("lonelyHeartsClub")
//也可以用@Named注解来为bean设置ID
@Named("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc{...}
2.2.3、设置组件扫描的基础包
默认会以配置类所在的包作为基础包(basepackage)来扫描组件。如果想扫描不同的包,或者扫描多个包,又该如何呢?
//指明包的名称
@Configuration
@ComponentScan("soundsystem")
public class CDPlayerConfig() {}
如果想更清晰地表明你所设置的是基础包,可以通过basePackages属性进行配置:
@Configuration
@ComponentScan(basePackages="soundsystem")
public class CDPlayerConfig() {}
多个基础包:basePackages={"soundsystem", "video"}
上面所设置的基础包是以String类型表示的,这种方法是类型不安全(not type-safe)的
//另外一种方法,将其指定为包种所包含的类或接口:
@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class })
public class CDPlayerConfig {}
如果所有的对象都是独立的,彼此之间没有任何依赖,就像SgtPeppersbean这样,那你所需要的可能就是组件扫描而已
但是很多对象会依赖其他的对象才能完成任务,这就需要一种方法能够将组件扫描得到的bean和它们的依赖装配在一起——Spring的自动装配
2.2.4、通过为bean添加注解实现自动装配
自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程种,会在Spring应用上下文种寻找匹配某个bean需求的其他bean
为了声明要进行自动装配,借助Spring的@Autowired注解
@Autowired注解不仅能用在构造器上,还能用在属性的Setter方法上:
@Autowired
public void setCompactDisc(CompactDisc cd) {
this.cd = cd;
}
在Spring初始化bean之后,它会尽可能得去满足bean的依赖,在上面的例子中,依赖是通过带有@Autowired注解的方法进行声明
假如有且只有一个bean匹配依赖需求的话,那这个bean将会被装配进来
如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常,为避免这个异常,可以:@Autowired(required=false)
当required设置为false时,Spring会尝试执行自动装配,但如果没有匹配的bean的话,Spring将会让这个bean处于未装配状态
如果你的代码没有进行null检查的话,这个处于未装配状态的属性有可能会出现NullPointerException
如果有多个bean满足依赖关系的话,Spring会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配
@Autowired还可以替换为@Inject——不推荐
2.3、通过Java代码装配bean
比如你想要将第三方库中的组件装配到你的应用中,这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了
要采用显式装配的方式。两种方案:Java和XML。
进行显示配置时,JavaConfig是更好的方案。因为它更强大、类型安全且对重构友好
JavaConfig是配置代码,意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码中,通常会将JavaConfig放到单独的包中
2.3.1、创建配置类
创建JavaConfig类的关键在于为其添加@Configuration注解,表明这个类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节
到此为止,我们都是依赖组件扫描来发现Spring应该创建的bean,下面将关注于显示配置
2.3.2、声明简单的bean
要在JavaConfig中声明bean,需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解,如下面的代码声明了CompactDisc bean:
@Bean
public CompactDisc sgtPepper() {
return new SgtPeppers();
}
@Bean注解会告诉Spring这个方法将会返回一个对象,该对象要注册为Spring应用上下文中的bean
2.3.3、借助JavaConfig实现注入
在JavaConfig中装配bean的最简单的方式就是引用创建bean的方法,如下就是一种声明CDPlayer的可行方案:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
在这里并没使用默认的构造器构造实例,而是调用了需要传入CompactDisc对象的构造器来创建CDPlayer实例
看起来,CompactDisc是通过调用sgtPeppers()得到的,但情况并非完全如此。
因为sgtPeppers()方法添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用
通过调用方法来引用bean的方式有点令人困惑。还有一种理解起来更简单的方式:
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
return new CDPlayer(compactDisc)
}
2.4、通过XML装配bean——Spring现在有了强大的自动化配置和基于Java的配置,XML不应该在是你的第一选择了
2.4.1、创建XML配置规范
在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。
在使用JavaConfig的时候,这意味着要创建一个带有@Configuration注解的类,而在XML配置中,意味着要创建一个以<beans>元素为根的XML文件
2.4.2、声明一个简单的<bean>:<bean>元素类似于JavaConfig中的@Bean注解
//创建这个bean的类通过class属性类指定的,并且要使用全限定的类名
//建议设置id,不要自动生成
<bean id="compactDisc" class="soundsystem.SgtPeppers" />
2.4.3、借助构造器注入初始化bean
//通过ID引用SgtPeppers
<bean id="cdPlayer" class="soundsystem.CDPlayer">
<constructor-arg ref="compactDisc" />
</bean>
当Spring遇到这个<bean>元素时,会创建一个CDPlayer实例。<constructor-arg>元素会告诉Spring要将一个ID为compactDisc的bean引用传递到CDPlayer的构造器中
当构造器参数的类型是java.util.List时,使用<list>元素。set也一样
<bean id="compactDisc" class="soundsystem.BlankDisc">
<constructor-arg value="The Beatles" />
<constructor-arg>
<list>
<ref bean="sgtPeppers />
<ref bean="whiteAlbum />
</list>
</constructor-arg>
</bean>
2.4.4、使用XML实现属性注入
通用规则:对强依赖使用构造器注入,对可选性依赖使用属性注入
<property>元素为属性的Setter方法所提供的功能与<constructor-arg>元素为构造器所提供的功能是一样的
<bean id="cdPlayer" class="soundsystem.CDPlayer">
<property name="compactDisc" ref="compactDisc" />
</bean>
借助<property>元素的value属性注入属性
<bean id="compactDisc" class="soundsystem.BlankDisc">
<property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
<property name="tracks">
<list>
<value>Getting Better</value>
<value>Fixing a Hole</value>
</list>
</property>
</bean>
2.5、导入和混合配置
2.5.1、在JavaConfig中引用XML配置
我们假设CDPlayerConfig已经很笨重,要将其拆分,方案之一就是将BlankDisc从CDPlayerConfig拆分出来,定义到它自己的CDConfig类中:
package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class CDConfig {
@Bean
public CompactDisc compactDisc() {
return new SgtPeppers();
}
}
然后将两个类在CDPlayerConfig中使用@Import注解导入CDConfig:
package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import; @Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
return new CDPlayer(compactDisc);
}
}
更好的办法是:创建一个更高级别的SoundSystemConfig,在这个类中使用@Import 将两个配置类组合在一起:
package soundsystem;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; @Configuration
@Import({CDPlayerConfig.class, CDConfig.class})
public class SoundSystemConfig {
}
如果有一个配置类配置在了XML中,则使用@ImportResource注解
@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {
}
2.5.2、在XML配置中引用JavaConfig
在JavaConfig配置中,使用@Import和@ImportResource来拆分JavaConfig类
在XML中,可以使用import元素来拆分XML配置
<beans ....>
<bean class="soundsystem.CDConfig" />
<import resource="cdplayer-config.xml" />
</beans>
不管使用JavaConfig还是使用XML进行装配,通常都会创建一个根配置,这个配置会将两个或更多的装配类/或XML文件组合起来
也会在根配置中启用组件扫描(通过<context:component-scan>或@ComponentScan)
小结:
Spring框架的核心是Spring容器。容器负责管理应用中组件的生命周期,它会创建这些组件并保证它们的依赖能够得到满足,这样的话,组件才能完成预定的任务
Spring中装配bean的三种主要方式:自动化配置、基于java的显式配置以及基于XML的显示配置。这些技术描述了Spring应用中的组件以及这些组件之间的关系
尽可能使用自动化装配,避免显式装配所带来的维护成本。基于Java的配置,它比基于XML的配置更加强大、类型安全并且易于重构
3、第三章:高级装配
本章内容:
- Spring profile
- 条件化的bean声明
- 自动装配与歧义性
- bean的作用域
- Spring表达式语言
3.1、环境与profile
开发软件的时候,一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。数据库配置、加密算法以及外部系统的集成是跨环境部署时会发生变化的典型例子
3.1.1、配置profile bean
Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大差别。在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean
不过spring并不是构建的时候做这个决策,而是运行时。这样的结果就是同一个部署单元(可能会是WAR文件)能够适用于所有的环境,没有必要重新构建
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile:
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig {
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuiler()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}
上面是在类级别上使用@Profile注解。从Spring3.2开始,也可以在方法级别上使用@Profile注解
在XML中配置profile
可以通过<beans>元素的profile属性,在XML中配置profile bean
//dev profile的bean
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:shema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
//qa profile的bean
<beans profile="qa">
<bean id="dataSource"
class= .../>
</beans>
3.1.2、激活profile
Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default
如果设置了spring.profiles.active属性的话,它的值就会用来确定哪个profile是激活的。如果没有,Spring将会查找spring.profiles.default的值
如果两者均没有设置,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean
有多种方式来设置这两个属性:
- 作为DispatcherServlet的初始化参数
- 作为web应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles注解设置
//在web应用的web.xml文件中设置默认的profile
//为上下文设置默认的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>
Spring提供了@ActiveProfiles注解,可以用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的profile
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
}
3.2、条件化的bean
@Condition注解,它可以用到带有@Bean注解的方法上。
如果给定的条件计算结果为true,就会创建这个bean,否则这个bean就会被忽略
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
可以看到,@Conditional中给定了一个Class,它指明了条件——也就是MagicExistsCondition
@Conditional将会通过Condition接口进行条件对比:
public interface Condition {
boolean matches(ConditionContext ctxt, AnnotatedTypeMetadata metadata);
}
设置给@Conditional的类可以是任意实现了Condition接口的类型。这个接口实现起来简单直接,只需提供matches()方法的实现即可
public class MagicExistsCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}
ConditionContext是一个接口,大致如下:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
通过ConditionContext,我们可以做到如下几点:
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么
- 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他注解,它也是一个接口:
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationType);
Map<String, Object> getAnnotationAttributes(String annotationType);
Map<String, Object> getAnnotationAttributes(String annotationType,boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
}
借助isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解。
借助其他的那些方法,能够检查@Bean注解的方法上其他注解的属性
Spring4开始,对@Profile注解进行了重构,使其基于@Conditional和Condition实现:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
//ProfileCondition检查某个bean profile是否可用
class ProfileCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : atrrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[])value))) {
return true;
}
}
return false;
}
}
return true;
}
}
ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助该信息,它会明确检查value属性,该属性包含了bean的profile名称
然后它根据通过ConditionContext得到的Environment来检查[借助acceptsProfiles()方法]该profile是否处于激活状态
3.3、处理自动装配的歧义性
假如我们使用@Autowired注解标注了setDessert()方法
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
本例中,Dessert是一个接口,且有三个类实现了这个接口,分别为Cake、Cookies和IceCream:
@Component
public class Cake implements Dessert { ... } @Component
public class Cookies implements Dessert { ... } @Component
public class IceCream implements Dessert { ... }
Spring没有办法识别应该自动装载哪一个,就会抛出NoUniqueBeanDefinitionException
发生歧义性的时候,可以将可选bean中的某一个设置为首先(primary),或使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean
3.3.1、标示首选的bean——@Primary注解:它只能标示一个优先的可选方案
@Component
@Primary
public class IceCream implements Dessert {...}
如果通过Java配置显式声明:
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
如果使用XML配置bean:
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />
3.3.2、限定自动装配的bean
Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件
//确保要将IceCream注入到setDessert()之中:
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
为@Qulifier注解所设置的参数就是想要注入的bean的ID——基于bean ID作为限定符问题:与要注入的bean的名称是紧耦合的
创建自定义的限定符——可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符
所要做的就是在bean声明上添加@Qualifier注解。例如,它可以与@Component组合使用:
@Component
@Qualifier("cold")
public class IceCream implements Desserts {...}
这种情况下,cold限定符分配给了IceCreambean,因为它没有耦合类名,因此可以随意重构IceCream的类名。
在注入的地方,只要引用cold限定符就可以了:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性术语
3.4、bean的作用域——@Scope注解
默认情况,Spring应用上下文所有bean都是单例(singleton)的形式创建的
Spring定义了多种作用域:
- 单例(singleton):在整个应用中,只创建bean的一个实例
- 原型(prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
- 会话(session):在web应用中,为每个会话创建一个bean实例
- 请求(request):在web应用中,为每个请求创建一个bean实例
//也可以使用@Scope("prototype")设置原型作用域,当不那么安全
@Component //如果使用组件扫描来发现和什么bean
//@Bean:在java配置中将Notepad声明为原型bean
@Scope(ConfigurabaleBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
同样,如果使用XML配置bean的话,可以使用<bean>元素的scope属性来设置作用域:
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
3.4.1、使用会话和请求作用域
在web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。
如,在电子商务应用中,可能会有一个bean代表用户的购物车,如果购物车是单例的话,那么将导致所有的用户都会向同一个购物车中添加商品
另一方面,如果购物车是原型作用域的,那么应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了
就购物车bean来说,会话作用域是最为合适的:在当前会话相关操作中,这个bean实际上相当于单例的
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() {...}
proxyMode=ScopedProxyMode.INTERFACES 这个属性解决了将会话或请求作用域的bean注入到单例中所遇到的问题
假如我们要将ShoppingCart bean注入到单例StoreSercice bean的Setter方法中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中
但是后者是会话作用域的,此时并不存在。
另外,系统中将会有多个ShoppingCart实例:每个用户一个;我们希望当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一个
Spring并不会将实际的ShoppingCart bean注入到StoreService中,Spring会注入一个到ShoppingCart bean的代理。
这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。
但是当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean
ScopedProxyMode.INTERFACES,表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean
如果ShoppingCart是接口而不是类的话,这是可以的(也是最理想的代理模式),如果它是一个具体的类的话,Spring就没有办法创建基于接口的代理了。
此时必须使用CGLib来生成基于类的代理,要将proxyMode设置为:ScopedProxyMode.TARGET_CLASS,表明要以生成目标类扩展的方式创建代理
3.4.2、在XML中声明作用域代理
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy />
</bean>
<aop:scoped-proxy>告诉Spring为bean创建一个作用域代理。默认,它会使用CGLib创建目标类的代理,可以要求它生成基于接口的代理:
<aop:scoped-proxy proxy-target-class="false" />
3.5、运行时值注入
当讨论依赖注入的时候,通常讨论的是将一个bean引用注入到另一个bean的属性或构造器参数中。它通常来讲是指的是将一个对象与另一个对象进行关联
但bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中
为避免硬编码,想让值在运行时再确定,为了实现这些功能,Spring提供了两种在运行时求值的方式:
- 属性占位符(Property placeholder)
- Spring表达式语言(SpEL)
3.5.1、注入外部的值
处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性:
//使用@PropertySource注解和Environment
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties") //声明属性源
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BlankDisc disc() {
return new BlanDisc(env.getProperty("disc.title"), env.getProperty("disc.artist")); //检索属性值
}
}
@PropertySource引用了类路径中一个名为app.properties的文件:
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
这个属性文件会加载到Spring的Environment中。稍后可从这里检索属性。同时,在disc()方法中,会创建一个新的BlankDisc,它的构造器参数是从属性文件中获取的,
而这是通过调用getProperty()实现的,它有四个重载的方法:
String getProperty(String key)
String getProperty(String key, String defaultValue)
T getProperty(String key, Class<T> type)
T getProperty(String key, Class<T> type, T defaultValue)
如果想检查一下某个属性是否存在的话,调用containsProperty()方法
boolean titleExists = env.containsProperty("disc.title");
如果想将属性解析为类的话,可以使用getPropertyAsClass()方法:
Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class", CompactDisc.class);
Environment还提供了一些方法来检查哪些profile处于激活状态:
String[] getActiveProfiles() //返回激活profile名称的数组
String[] getDefaultProfiles() //返回默认profile名称的数组
boolean acceptsProfiles(String... profiles) //如果environment支持给定profile的话,就返回true
解析属性占位符
在Spring装配中,占位符的形式为使用 ${...} 包装的属性名称:
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}" />
如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,可以使用@Value注解:
public BlankDisc(@Value("${disc.title}" String title, @Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}
为了使用占位符,要配置PropertySourcesPlaceholderConfigurer。
如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer:
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
如果使用XML配置的话,Spring context命名空间中的<context:propertyplaceholder>元素将会为你生成PropertySourcesPlaceholderConfigurer bean
3.5.2、使用Spring表达式语言进行装配:强大简洁
- 使用bean的ID来引用bean
- 调用方法和访问对象的属性
- 对值进行算术、关系和逻辑运算
- 正则表达式匹配
- 集合操作
SpEL表达式要放到“#{...}”之中
//通过systemProperties对象引用系统属性:
#{systemProperties['disc.title']}
注意:不要让你的表达式太智能,否则测试越只要。建议尽可能让表达式保持简洁
4、第四章:面向切面的Spring
本章内容:
- 面向切面编程的基本原理
- 通过POJO创建切面
- 使用@AspectJ注解
- 为AspectJ切面注入依赖
在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑中
切面能帮助我们模块化横切关注点。简单的说,横切关注点可以被描述为影响应用多处的功能。例如:安全
如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。
但是如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系
而使用委托可能需要对委托对象进行复杂的调用
使用切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类
好处:
首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;
其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了
4.1.1、定义AOP术语
常用术语:通知(advice)、切点(pointcut)和连接点(join point)
通知(advice):切面的工作被称为通知。通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
连接点(Join point):是在应用执行过程种能够插入切面的一个点
切点(Pointcut):如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”
切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能
引入(Introduction):引入允许我们向现有的类添加新方法或属性
织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码,如AspectJ5的加载时织入
- 运行期:切面在应用运行的某个时刻被织入。织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的
4.1.2、Spring对AOP的支持
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各版本)
现在的Spring引入了简单的声明式AOP和基于注解的AOP之后,Spring经典的AOP就 显得过于笨重和复杂
借助Spring的aop命名空间,我们可以将纯POJO转换为切面,但是需要XML配置
Spring借鉴了AspectJ的切面,以提供注解驱动的AOP。本质上,它依然是Spring基于代理的AOP,好处在于不使用xml来完成
如果你的AOP需求超过了简单的方法调用(如构造器或属性拦截),那就需要考虑使用AspectJ来实现切面
Spring通知是Java编写的——Spring所创建的通知都是用标准的Java类编写的
Spring在运行时通知对象——通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中
如下图,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑
因为Spring基于动态代理,所以Spring只支持方法连接点,这与其他的AOP框架是不同的,如AspectJ
Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,如拦截对象字段的修改,而且它不支持构造器连接点,我们就无法在bean创建时应用通知
4.2、通过切点来选择连接点
//为阐述Spring中的切面,需要有个主题来定义切面的切点。为此,我们定义了一个Performance接口:
package concert;
public interface Performance {
public void perform();
}
假设我们要编写Performance的perform方法触发通知:
假如需要配置的切点仅匹配concert包,可以使用within()指示器来限制匹配:
在Spring的XML配置里面描述切点时,使用and,or,not来代替&&,||,!
4.2.2、在切点中选择bean
//限定bean的ID为Woodstock
execution(* concert.Performance.perform()) and bean('woodstock')
4.3、使用注解创建切面——使用注解来创建切面是AspectJ 5 引入的关键特性
//Audience类:观看演出的观众的切面
@Aspect
public class Audience {
//通过@Pointcut注解声明频繁使用的切点表达式
@Pointcut("execution(** concert.Perforance.perform(...))")
public void performance() {}
//表演之前:未使用Pointcut
@Before("execution(** concert.Perforance.perform(...))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
//表演之后
@AfterReturnint("performance() ")
public void applause() {
System.out.println("CLAP CLAP CLAP");
}
//表演失败之后
@AfterThrowing("performance() ")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}
需要注意,Audience类依然是一个POJO,像其他Java类一样,可以装配为Spring中的bean:
@Bean
public Audience audience() {
return new Audience();
}
如果就此止步的话,Audience只会是Spring容器中的一个bean,即使使用了AspectJ注解,也不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理
如果使用JavaConfig的话,只需
@Configuration
@EnableAspectJAutoProxy //启用AspectJ 自动代理
@ComponentScan
public bean ConcertConfig {
@Bean
public Audience audience() { //声明Audience bean
return new Audience();
}
}
如果使用XML来装配bean的话,使用aop命名空间中的<aop:aspectj-autoproxy>元素:
<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy /> //启用AspectJ 自动代理
<bean class="concert.Audience" /> //声明Audience bean
Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。本质上,它依然是Spring基于代理的切面
4.3.2、创建环绕通知
@Aspect
public class Audience {
@Pointcut("execution(** concert.Performance.perform(...))")
public void performance() {}
//环绕通知方法
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP CLAP CLAP!");
} catch (Throwable e) {
System.out.println("Demanding a refund");
}
}
}
接受ProceedingJoinPoint作为参数,这个对象是必须有的,要在通知中通过它来调用被通知的方法。
当将控制权交给被通知的方法时,它需要调用ProceeddingJoinPoint的proceed()方法。如果不调用此方法,你的通知实际上会阻塞对被调用方法的调用
4.3.3、处理通知中的参数
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
@Pointcut("execution(* soundsystem.CompactDisc.playTrack(int))" + "&& args(trackNumber)") //通知play-Track()方法
public void trackPlayed(int trackNumber) {}
@Before("trackPlayed(trackNumber)") //在播放前,为该磁道计数
public void countTrack(int trackNumber) {
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}
public int getPlayCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
现在我们可以在Spring配置中将BlankDisc和TrackCounter定义为bean,并启用AspectJ自动代理:
//配置TrackCount记录每个磁道播放的次数
@Configuration
@EnableAspectJAutoProxy //启用AspectJ自动代理
public class TrackCounterConfig {
@Bean
public CompactDisc sgtPeppers () { //CompactDisc bean
BlankDisc cd = new BlankDisc();
cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");
cd.setArtist("The Beatles");
List<String> tracks = new ArrayList<String>();
tracks.add("Sgt. Pepper's Lonely Hearts Club Band");
tracks.add("With a Little Help from My Friends");
tracks.add("Fixing a Hole");
cd.setTracks(tracks);
return cd;
}
@Bean
public TrackCounter trackCounter() { //TrackCounter bean
retrun new TrackCounter();
}
}
最后,测试TrackCounter 切面
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TrackCounterConfig.class)
public class TrackCounterTest {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@Autowired
private CompactDisc cd;
@Autowired
private TrackCounter counter;
@Test
public void testTrackCounter() {
cd.playTrack(1); //播放一些磁道
cd.playTrack(2);
cd.playTrack(3);
cd.playTrack(3);
//断言期望的数量
assertEquals(1, counter.getPlayCount(1));
assertEquals(1,counter.getPlayCount(2));
assertEquals(4,counter.getPlayCount(3));
assertEquals(0,counter.getPlayCount(4));
}
}
目前为止,所使用的切面中,所包装的都是被通知对象的已有方法。下面看一下如何通过编写切面,为被通知的对象引入全新的功能
4.3.4、通过注解引入新功能
动态语言,可以不用直接修改对象或类的定义就能够为对象或类增加新的方法。但Java不是动态语言,类编译完成了,就很难为该类添加新的功能了
切面可以为Spring bean添加新方法:
为示例中的所有的Performance实现引入下面的Encoreable
package concert;
public interface Encoreable {
void performEncore();
}
借助AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现,为了实现该功能,我们要创建一个新的切面:
package concert;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
可以看到,EncoreableIntroducer是一个切面,但是和我们之前创建的切面不同,它并没有提供前置、后置或环绕通知,而是通过@DeclareParents注解,将Encoreable接口引入到Performance bean中
@DeclareParents注解由三部分组成:
- value属性指定了哪种类型的bean要引入该接口。标记符后面的加号表示是Performance的所有子类型,而不是Performance本身
- defaultImpl属性指定了为引入功能提供实现的类。这里我们指定DefaultEncoreable提供实现
- @DeclareParents注解所标注的静态属性指明了要引入了接口。这里我们所引入的是Encoreable接口
和其他的切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean:
<bean class="concert.EncoreableIntroducer" />
Spring的自动代理机制将会获取到它的声明,当Spring发现一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口
在Spring中,注解和自动代理提供了一种很便利的方式来创建切面。但是,你必须能够为通知类添加注解,为了做到这一点,必须要有源码。
4.4、在XML中声明切面
基于注解的配置要优于基于Java的配置,基于Java的配置要优于基于XML的配置。
但如果你需要声明切面,但是又不能为通知类添加注解的时候,那么就必须专向XML配置了