面向切面编程是 Spring 最为重要的功能之一,在数据库事务管理中广泛应用。
全部章节传送门:
Spring学习笔记(一):Spring IoC 容器
Spring学习笔记(二):Spring Bean 装配
Spring学习笔记(三): Spring 面向切面
Spring学习笔记(四): Spring 数据库编程
Spring学习笔记(五): Spring 事务管理
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
AOP 常用术语
- 切面(Aspect): 通常是一个类,里面可以定义切入点和通知。
- 通知(Advice): 通知是切面开启后,切面的方法,它根据在代理对象真实方法调用前、后的顺序和逻辑区分,一共五类:
- 前置通知(before): 在动态代理反射原有对象或者执行环绕通知前执行的通知功能。
- 后置通知(after): 在动态代理反射原有对象或者执行环绕通知后执行的通知功能,无论是否抛出异常,它都会被执行。
- 返回通知(afterReturning): 在动态代理反射原有对象或者执行环绕通知后正常返回(无异常)执行的通知功能。
- 异常通知(afterThrowing): 在动态代理反射原有对象或者执行环绕通知产生异常后执行的通知功能。
- 环绕通知(around): 在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。
- 引入(Introduction): 在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
- 连接点(join point): 被拦截到的点。
- 切点(Pointcut): 对连接点进行拦截的定义。
- 织入(Weaving): 将切面应用到目标对象并导致代理对象创建的过程。
Spring 对 AOP 的支持
Spring AOP 是一种基于方法拦截的 AOP,它只支持方法拦截。在 Spring 中有4种方法去实现 AOP 的拦截功能。
- 使用 ProxyFactoryBean 和对应的接口实现 AOP。
- 使用 XML 配置 AOP。
- 使用 @AspectJ 注解驱动切面。
- 使用 AspectJ 注入切面。
其中,真正常用的是 @AspectJ 注解,有时候 XML 配置也有一定的辅助作用,另外2种很少使用。
使用 @AspectJ 注解开发 Spring AOP
使用 @AspectJ 注解的方式是当下的主流方式。
创建一个 maven 项目, 引入 Spring 相关依赖。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
选择连接点
创建一个接口,使用其中的方法作为连接点。
package com.wyk.aopdemo.service;
import com.wyk.aopdemo.domain.Role;
public interface RoleService {
public void printRole(Role role);
}
然后编写它的实现类。
package com.wyk.aopdemo.service.impl;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.stereotype.Component;
@Component
public class RoleServiceImpl implements RoleService {
public void printRole(Role role) {
System.out.println("{id: " + role.getId() + ", role_name: "
+ role.getRoleName() + ", note: " + role.getNote() + "}");
}
}
其中的 Role 是一个实体类。
package com.wyk.aopdemo.domain;
public class Role {
private Long id;
private String roleName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
创建切面和切点
在类上添加 @Aspect 注解即可将其变成一个切面。
package com.wyk.aopdemo.aspect;
import org.aspectj.lang.annotation.*;
@Aspect
public class RoleAspect {
@Pointcut("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))")
public void print() {
}
@Before("print()")
public void before() {
System.out.println("before ...");
}
@After("print()")
public void after() {
System.out.println("after ...");
}
@AfterReturning("print()")
public void afterReturning() {
System.out.println("afterReturn ...");
}
@AfterThrowing("print()")
public void afterThrowing() {
System.out.println("afterThrowing ...");
}
}
其中的 @Pointcut 注解用来定义切点,其他注解代表通知,很好理解。如过不定义切点则需要在通知上直接写正则表达式,比较麻烦。
对切点中的正则表达式简单分析一下。
execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))
- execution: 代表执行方法的时候会触发。
- *:代表任意返回类型的方法。
- com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole: 代表类的全限定名。
- (..): 任意的参数。
生成 AOP 实例
最后需要添加方法配置 Spring 的 Bean 。
package com.wyk.aopdemo.config;
import com.wyk.aopdemo.aspect.RoleAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.wyk.aopdemo")
public class AppConfig {
@Bean
public RoleAspect getRoleAspect() {
return new RoleAspect();
}
}
其中 @EnableAspectJAutoProxy 注解用来启用 AspectJ 框架的自动代理,这个时候 Spring 会生成动态代理对象,进而可以使用 AOP , 而其中的方法用来生成一个切面实例。
进行测试
编写一个测试类进行测试。
package com.wyk.aopdemo;
import com.wyk.aopdemo.config.AppConfig;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
Role role = new Role();
role.setId(1L);
role.setRoleName("wyk");
role.setNote("haha");
roleService.printRole(role);
System.out.println("############################");
//测试异常通知
role = null;
roleService.printRole(role);
}
}
运行程序,在控制台查看结果。很显然切面的通知已经通过 AOP 织入约定的流程当中。
before ...
{id: 1, role_name: wyk, note: haha}
after ...
afterReturn ...
############################
before ...
after ...
afterThrowing ...
环绕通知
环绕通知是 Spring AOP 当中最强大的 AOP ,它可以同时实现前置通知和后置通知。它保留了调度被代理对象原有方法的功能,所以它既强大又灵活,但是可控性不强,如果不需要大量改变业务逻辑,一般而言并不需要使用它。
在 RoleAspect 类中添加环绕通知方法。
@Around("print()")
public void around(ProceedingJoinPoint jp) {
System.out.println("around before ...");
try {
jp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("around after ...");
}
在一个切面里通过 @Around 注解加入环绕通知,其中方法中有一个 ProceedingJoinPoint 参数,它由 Spring 提供,可以反射连接点方法。
运行程序,查看结果。
around before ...
before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after ...
afterReturn ...
############################
around before ...
before ...
...(异常堆栈信息)
around after ...
after ...
afterReturn ...
根据控制台输出,可以看到执行顺序。
织入
织入是生成代理对象并把切面内容放入约定流程的过程,上述示例中连接点所在的类都是拥有接口的类。事实上即使没有接口,Spring 也能提供 AOP 功能,但是在 Spring 中建议使用接口编程,因为这样的好处是使定义和实现分离,有利于变化和替换,更加灵活。
给通知传递参数
如果需要给通知传递参数的话,则需要修改通知方法。首先修改连接点为一个多参数的方法。
public void printRole(Role role, int sort) {
System.out.println("{id: " + role.getId() + ", role_name: "
+ role.getRoleName() + ", note: " + role.getNote() + "}");
System.out.println(sort);
}
这样在通知中应该如下定义方法。
@Before("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..)) && args(role, sort)")
public void before(Role role, Sort sort) {
System.out.println("before ...");
}
引入
有时候,我们希望在流程中引入其它类的方法来得到更好的实现,这时需要使用 Spring 的引入技术。
首先新建一个接口,用来检查角色是否为空。
package com.wyk.aopdemo.service;
import com.wyk.aopdemo.domain.Role;
public interface RoleVerifier {
public boolean verify(Role role);
}
编写其实现类。
package com.wyk.aopdemo.service.impl;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleVerifier;
import org.springframework.stereotype.Component;
public class RoleVerifierImpl implements RoleVerifier {
public boolean verify(Role role) {
return role != null;
}
}
在切面 RoleAspect 类中添加一个新属性,用来将 RoleVerifier 加入到切面之中。
@DeclareParents(value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+",
defaultImpl = RoleVerifierImpl.class)
public RoleVerifier roleVerifier;
注解 @DeclareParents 用来引入类。
- value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+" 表示对 RoleServiceImpl 类进行增强,也就是在 RoleServiceImpl 中添加一个新的接口。
- defaultImpl 代表默认的实现类。
修改测试方法,进行测试。
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
RoleVerifier roleVerifier = (RoleVerifier)roleService;
Role role = new Role();
role.setId(1L);
role.setRoleName("wyk");
role.setNote("haha");
if(roleVerifier.verify(role)) {
roleService.printRole(role);
}
}
}
此方法的原理是让代理对象挂到 RoleService 和 RoleVerifier 两个接口之下,这样就可以在它们之间进行强制转换。
使用 XML 配置开发 Spring AOP
使用 XML 方式开发 AOP 和使用注解的原理相同。
接口和接口实现类和上面的相同,这里不在描述。切面类和前面的例子也相同,只是不需要添加注解。
package com.wyk.xmlaop.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
public class XmlAspect {
public void before() {
System.out.println("before ...");
}
public void after() {
System.out.println("after ...");
}
public void afterThrowing() {
System.out.println("after-throwing ...");
}
public void afterReturning() {
System.out.println("after-returning ...");
}
public void around(ProceedingJoinPoint jp) {
System.out.println("around before ...");
try {
jp.proceed();
} catch (Throwable e) {
new RuntimeException("回调原有流程,产生异常 ...");
}
System.out.println("around after ...");
}
}
添加通知
在类路径下面添加 XML 配置文件 spring-cfg.xml 。
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
">
<bean id="xmlAspect" class="com.wyk.xmlaop.aspect.XmlAspect" />
<bean id="roleService" class="com.wyk.xmlaop.service.impl.RoleServiceImpl" />
<aop:config>
<!-- 引用xmlAspect作为切面 -->
<aop:aspect ref="xmlAspect">
<!--定义切点-->
<aop:pointcut id="printRole" expression="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..))" />
<!--定义通知,引入切点-->
<aop:before method="before" pointcut-ref="printRole" />
<aop:after method="after" pointcut-ref="printRole" />
<aop:after-throwing method="afterThrowing" pointcut-ref="printRole" />
<aop:after-returning method="afterReturning" pointcut-ref="printRole" />
<aop:around method="around" pointcut-ref="printRole" />
</aop:aspect>
</aop:config>
</beans>
在文件中通过<aop:config>定义 AOP 的内容信息。
- <aop:aspect> 定义切面类。
- <aop:before> 定义前置通知。
- <aop:after> 定义后置通知。
- <aop:after-throwing> 定义异常通知。
- <aop:after-returning> 定义返回通知。
- <aop:around> 定义环绕通知。
进行测试
创建测试类进行测试。
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
RoleService roleService = ctx.getBean(RoleService.class);
Role role = new Role();
role.setId(1L);
role.setRoleName("wyk");
role.setNote("haha");
roleService.printRole(role);
}
}
运行程序,查看结果。
before ...
around before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after-returning ...
after ...
这里的顺序貌似和注解版不一样,不知道为啥。。。
给通知添加参数
修改 before 方法。
public void before(Role role) {
System.out.println("before ...");
}
然后 XML 中的配置也要修改。
<aop:before method="before" pointcut="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..)) and aegs(role)" />
和注解版不同的是这里使用 and 而不是 && ,这是因为 & 是 xml 保留字符。
引入
引入方法和上面的例子相同,这里不再赘述,其中 XML 需要添加如下配置。
<aop:declare-parents type-matching="com.wyk.xmlaop.service.impl.RoleServiceImpl+"
implement-interface="com.wyk.xmlaop.service.RoleVerifier"
default-impl="com.wyk.xmlaop.service.RoleVerifierImpl" />
多个切面
Spring 中也能支持一个连接点包含多个切面,但多个切面之间的顺序是随机的,可以在切面类上使用 @Order注解进行排序。如果是 XML 配置文件,也可以在配置标签上添加 order 属性。