推荐阅读官方文档:↓↓↓

Spring官方文档: 官方文档
W3Cschool中文文档: 中文文档

控制反转 IOC

面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式地用 new 创建 B 的对象。采用依赖注入技术之后,A 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。
实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。

依赖注入 DI

依赖注入有如下实现方式:

  • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
  • 基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
  • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
  • 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。

依赖查找 DL

依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。

Spring的IOC容器配置

使用配置文件

XML配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>

<bean id="bean1" class="cf.vbnm..."/>
<bean name="bean2" class="cf.vbnm..."/>

<!-- more bean definitions for data access objects go here -->

</beans>

Java初始化

1
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

考虑下面的文件结构:

1
2
3
4
5
com/
example/
services.xml
repositories.xml
MessengerService.class

可以这样初始化:

1
2
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "repositories.xml"}, MessengerService.class);

C命名空间 c-namespace

从Spring3.1开始, 允许使用内联属性配置构造器参数而不是使用嵌套的<constructor-arg>标签进行配置.

C是constructor的第一个字母
下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>

<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="[email protected]"/>
</bean>

<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="[email protected]"/>

</beans>

有一些不常见的例子: 构造器参数名不可用, 通常情况是编译后的字节码没有调试信息, 你可以使用后备方案: 使用位置索引指定参数. 例子如下:

```XML
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="[email protected]"/>

对于XML语法来说, 参数位置符号前需要加上一个前导_, 因为XML属性不能以_开头(即使一些IDE支持).<constructor-arg>元素也可以使用相应的索引符号, 但是这不常用, 因为它的声明顺序通常就足以表示了.

官方文档:XML Shortcut with the c-namespace

P命名空间 p-namespace

P命名空间与C命名空间使用方法类似, 不过P指代Property.
下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="[email protected]"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="[email protected]"/>
</beans>

使用注解

创建一个配置类, 在类上标记@Configuration注解, 创建一些方法, 在方法中生产对象并返回, 同时在方法上标记@Bean注解, 可以使用@Scope("prototype")指定Scope. 相当于配置文件中的<bean name="bean2" class="cf.vbnm..."/>.

1
2
3
4
5
6
7
8
9
@org.springframework.context.annotation.Configuration  
public class Configuration {

@Bean
@Scope("prototype")
public WebSocketFactory getWebSocketFactory() {
return new DefaultWebSocketServerFactory();
}
}

Ant-Style

Ant风格(Ant Style):该风格源自Apache的Ant项目. Ant风格简单的讲,它是一种精简的匹配模式,仅用于匹配路径或目录。

Ant风格虽然概念源自Apache Ant,但是借由Spring“发扬光大”。在整个Spring体系内,涉及到的URL匹配、资源路径匹配、包名匹配均是支持Ant风格的,底层由接口PathMatcher的唯一实现类AntPathMatcher提供实现。

Ant风格简单的讲,它是一种精简的匹配模式,仅用于匹配路径or目录。使用大家熟悉的(这点很关键)的通配符:

通配符 说明
? 匹配任意单字符
* 匹配任意数量的字符
** 匹配任意层级的路径/目录

AntPathMatcher不仅可以匹配Spring的@RequestMapping路径,也可以用来匹配各种字符串,包括文件资源路径、包名等等。由于它所处的模块是spring-core无其它多余依赖,因此若有需要(比如自己在写框架时)我们也可以把它当做工具来使用,简化开发。

Spring的DI使用

注解

可以在成员变量上标注@Autowired来进行自动依赖注入, 或者在有参构造器上标注@Autowired进行自动依赖注入.
如有多个相同类型的bean注册到Spring, 使用@Qualifier("bean_name")来指定注入的bean.

1
2
3
4
5
6
7
8
9
10
11
public DependenceInjection{

// @Autowired //不推荐, 可能会遇到反射无法写入的问题, 导致注入失败
private Bean bean;

@Autowired //推荐使用, 可以避免反射反射带来的问题
public DependenceInjection(Bean bean){
this.bean = bean;
}

}

更多用法可以查看官方文档:1.9. Annotation-based Container Configuration

配置文件

在配置文件中定义一个bean1, bean2中需要一个bean1对象, 当需要bean2的时候, bean1会自动的注入到bean2中, 而不需要手动创建bean1, 实现依赖注入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<bean name="bean1" class="cf.vbnm.demo.Bean1"/>
<bean name="bean2" class="cf.vbnm.demo.Bean2">
<constructor-arg name="bean" ref="bean1"/>
</bean>

</beans>

在配置文件中开启注解扫描

在配置文件中加入以下以下内容.

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

</beans>

面向切面编程 AOP

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计剖面导向程序设计),是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。
Spring框架的关键组件之一是面向方面编程(AOP)。 面向方面的编程需要将程序逻辑分解成不同的部分。 跨越应用程序的多个点的功能被称为交叉切割问题,这些交叉关切在概念上与应用程序的业务逻辑分开。有如:日志记录,审计,声明式事务,安全性和缓存等方面的各种常见的的例子。

OOP模块化的关键单位是类,而在AOP中,模块化单位是方面。 依赖注入可帮助您将应用程序对象彼此分离,并且AOP可帮助您将交叉问题与其影响的对象分离。AOP就像一个触发器。

SpringAOP原理

Spring的AOP通过动态代理生成增强后的对象, 从而实现AOP的功能. 从Spring容器获取到的被AOP增强的对象已经不是原来的类了, 而是动态生成的. 配置好切点和增强方法后, Spring会动态生成被代理的类来实现方法的增强, 被增强的类必须由Spring管理 (配置@Component等) 并由Spring注入 (配置@Autowired等) 或取出 (使用ctx.getBean("name")等), 直接new是无法通过SpringAOP增强的.
简单来说就是Spring根据配置动态的加入增强的内容并生成新的增强后的对象.
AOP的操作对象是方法.

术语

连接点 JoinPoint

程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理. 可以理解成潜在的触发点. 在Spring中, 连接点总是方法.

切点 Pointcut

匹配 join point 的谓词(a predicate that matches join points). Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行.
相当于选择所需要的一系列触发点.

增强 Advice

由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.
相当于相当于触发前后所需要做的事情.

AOP 注解使用

使用@EnableAspectJAutoProxy开启注解.

@Aspect

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class SystemArchitecture {

@Pointcut("execution(* *(..))")
public void log() {}
}

Spring文档建议将切点全部放在SystemArchitecture类中方便管理.

@Pointcut

@Pointcut注解添加到方法上, 如上例, 参数内容为切点表达式.
切点的表达式以指示器开始, 指示器就是一种关键字,用来告诉 SpringAOP 如何匹配连接点,SpringAOP 提供了以下几种指示器.

  • execution
  • within
  • this 和 target
  • args
  • @target
  • @annotation

execution

该指示器用来匹配方法执行连接点,即匹配哪个方法执行,如:

1
@Pointcut("execution(public String aaric.springaopdemo.UserDao.findById(Long))")

上面这个切点会匹配在 UserDao 类中 findById 方法的调用,并且需要该方法是 public 的,返回值类型为 String,只有一个 Long 的参数。
切点的表达式同时还支持宽字符匹配,如:

1
@Pointcut("execution(* aaric.springaopdemo.UserDao.*(..))")

上面的表达式中,第一个宽字符 * 匹配 任何返回类型,第二个宽字符 * 匹配 任何方法名,最后的参数 (..) 表达式匹配 任意数量任意类型 的参数,也就是说该切点会匹配类中所有方法的调用.

within

如果要匹配一个类中所有方法的调用,便可以使用 within 指示器.

this

用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;注意this中使用的表达式必须是完整类名,不支持通配符.

target

用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是完整类名,不支持通配符.

  • 如果目标对象实现了任何接口,Spring AOP 会创建基于JDK 的动态代理,这时候需要使用 target 指示器
  • 如果目标对象没有实现任何接口,Spring AOP 会创建基于CGLIB的动态代理,这时候需要使用 this 指示器

一般情况下代理类(Proxy)和目标类(Target)都实现了相同的接口,所以上面的2个基本是等效的。

args

该指示器用来匹配具体的方法参数.

1
@Pointcut("execution(* *..find*(Long))")

这个切点会匹配任何以 find 开头并且只有一个 Long 类型的参数的方法。
如果我们想匹配一个以 Long 类型开始的参数,后面的参数类型不做限制,我们可以使用如下的表达式:

1
@Pointcut("execution(* *..find*(Long,..))")

@target

该指示器不要和 target 指示器混淆,该指示器用于匹配连接点所在的类是否拥有指定类型的注解,如:

1
@Pointcut("@target(org.springframework.stereotype.Repository)")

@annotation

该指示器用于匹配连接点的方法是否有某个注解.

1
@Pointcut("@annotation(org.springframework.scheduling.annotation.Async)")

bean

Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法.

reference pointcut

表示引用其他命名切入点,只有注解风格支持,XML风格不支持.

组合切点表达式

切点表达式可以通过 &&、 || 和 !等操作符来组合,如

1
2
3
4
5
6
7
8
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}

@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}

@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}

上面的第三个切点需要同时满足第一个和第二个切点表达式

Advice

Advice共有如下几种

  • Before Advice: @Before
  • After Advice: @After
  • Around Advice: @Around
  • After throwing Advice: @AfterThrowing
  • After return Advice: @AfterReturning

通知执行顺序:前置通知→环绕通知连接点之前→连接点执行→环绕通知连接点之后→返回通知→{ 后通知 || [(如果发生异常)异常通知→后通知] }

注解包含的参数为之前定义的切点方法名.