Spring学习(四):AOP详解

概述

AOP是aspect-oriented programming的缩写,译为面向切面编程。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得 业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。简单来说,AOP就是不修改源代码在主干功能里面添加新功能。

底层原理

AOP底层使用了动态代理:在有接口的时候使用JDK 动态代理、在没有接口的时候使用CGLIB字节码动态代理

JDK动态代理

简介

使用JDK 动态代理需要用到JDK中的Proxy类里面的newProxyInstance方法创建代理对象。方法如下:

1
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

newProxyInstance方法的三个参数说明:

  • loader 定义代理类的类加载器
  • interfaces 代理类要实现的接口列表(可以多个)
  • h 指派方法调用的处理程序(要增强的功能在这里面实现)

newProxyInstance方法返回一个指定接口的代理类实例。

InvocationHandler接口中有个invoke方法,用于实现增强的功能:

1
2
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;

invoke方法的三个参数说明:

  • proxy表示代理对象
  • method表示被增强的方法
  • args是方法的参数 若没有则为null

示例代码

代码结构如下:

1
2
3
4
5
6
└─src
└─com
└─spring5
JDKProxy.java
UserDao.java
UserDaoImpl.java

UserDao接口:

1
2
3
4
public interface UserDao {
int add(int a,int b);
String update(String id);
}

UserDaoImpl类:

1
2
3
4
5
6
7
8
9
10
11
12
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
System.out.println("I am add...");
return a+b;
}

@Override
public String update(String id) {
return id;
}
}

JDKProxy类:

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
public class JDKProxy {
public static void main(String[] args) {
Class[] interfaces = {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
UserDao dao =(UserDao)Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces,
new UserDaoProxy(userDao));
int result = dao.add(1, 2);
System.out.println("result:"+result);
}
}

//生成代理对象
class UserDaoProxy implements InvocationHandler {

// 用有参构造方法来接收传递过来的待代理对象*,比如UserDaoImpl
private Object obj;
public UserDaoProxy(Object obj) {
this.obj = obj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法之前执行...." + method.getName() + " :传递的参数..." + Arrays.toString(args));
Object res = method.invoke(obj, args);
System.out.println("方法之后执行...." + obj);
return res;
}
}

运行结果:
方法之前执行…add :传递的参数…[1, 2]
I am add…
方法之后执行…com.spring5.UserDaoImpl@355da254
result:3

CGLIB字节码动态代理

简介

使用CGLIB字节码动态代理不受代理类必须实现接口的限制,其底层采用ASM字节码生成框架。CGLIB动态代理的优缺点:

  • 使用字节码技术生产代理类比JAVA反射效率高
  • 不能对声明为final的类和方法进行代理,因为其原理是动态生成被代理类的子类

需要实现接口MethodInterceptor,然后重写intercept方法:

1
Object intercept(Object proxy, Method method, Object[] args, MethodProxy arg3) throws Throwable;

intercept方法的参数说明:

  • proxy CGLIB生成的代理类实例,也是目标对象的子类
    相当于重写父类方法
  • method 被代理方法
  • args 方法参数
  • 为生成的代理类对方法的代理引用
    intercept方法返回
    参考链接

另外用到了Enhancer类,它是Cglib中的一个字节码增强器,先调它的setSuperclass()将被代理类设置成父类、再调setCallback函数执行intercept方法,最后调create()生成代理类。

示例代码

代码结构如下:

1
2
3
4
5
└─src
└─com
└─spring5
CglibProxy.java
User.java

User类:

1
2
3
4
5
public class User {
public void sleep() {
System.out.println("我想睡觉...");
}
}

CglibProxy类:

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
public class CglibProxy {
public static void main(String[] args) {
UserProxy userProxy = new UserProxy();
User base=(User) userProxy.getProxy(new User());
base.sleep();
}
}

//生成代理对象
class UserProxy implements MethodInterceptor {

public Object getProxy(Object object){
Enhancer e=new Enhancer();
e.setSuperclass(object.getClass());
e.setCallback(this);//设置回调函数,即调用intercept()
return e.create();
}

@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy arg3) throws Throwable {
System.out.println("睡觉前脱衣服");
Object object = arg3.invokeSuper(proxy, args);
System.out.println("起床穿衣服");
return object;
}
}

运行结果:
睡觉前脱衣服
我想睡觉…
起床穿衣服

AOP操作

概述

AOP相关的几个术语:

  • 连接点
    类里面哪些方法可以被增强,这些方法称为连接点
  • 切入点
    实际被真正增强的方法称为切入点
  • 通知
    实际增强的逻辑部分称为通知,分为前置通知、后置通知、环绕通知、异常通知和最终通知五种类型,其中最终通知相当于JAVA的finally
  • 切面
    把通知应用到切入点过程

AspectJ

AspectJ不是 Spring 组成部分,是一个独立的AOP 框架,一般把 AspectJ 和 Spirng 框架一起使 用,进行 AOP操作。增强就是代理的意思

准备工作

在进行AOP操作的时候需要先引入下面四个Jar包

1
2
3
4
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aspects-5.2.6.RELEASE.jar

引入包后的所有包如下:

  • com.springsource.net.sf.cglib-2.2.0.jar
  • com.springsource.org.aopalliance-1.0.0.jar
  • com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
  • spring-aspects-5.2.6.RELEASE.jar
  • commons-logging-1.1.1.jar
  • spring-aop-5.2.6.RELEASE.jar
  • spring-beans-5.2.6.RELEASE.jar
  • spring-context-5.2.6.RELEASE.jar
  • spring-core-5.2.6.RELEASE.jar
  • spring-expression-5.2.6.RELEASE.jar

AspectJ的切入点表达式语法说明

  • 语法
1
execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]) )
  • 示例语法详细说明
1
execution (* com.sample.service.impl..*. *(..))

1、execution(): 表达式主体。
2、第一个*号:表示返回类型, 号表示所有的类型。
3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
4、第二个
号:表示类名,号表示所有的类。
5、
(…):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

  • 其他示例
1
2
3
4
5
6
7
8
9
10
//例1 对com.dao.BookDao 类里面的 add 进行增强
execution(* com.dao.BookDao.add(..))
// 例2 对多个方法进行增强 用||和or都可以
execution(* com.dao.BookDao.add(..)) || excution(* com.dao.BookDao.delete(..))
@AfterReturning(value = "execution(* com.dao.BookDao.add(..)) || execution(* com.dao.BookDao.delete(..))")

//例3 对com.dao.BookDao 类里面的所有的方法进行增强
execution(* com.dao.BookDao.* (..))
//例4 对com.dao 包里面所有类里面的所有方法进行增强
execution(* com.dao.*.* (..))

基于注解实现

主要步骤

主要步骤如下:

  • 在Spring配置文件中,开启注解扫描
    需要在XML中引入context和aop的上下文空间。

  • 使用注解创建 User 和 UserProxy 对象

  • 在增强类上面添加注解 @Aspect

  • 在Spring配置文件中开启生成代理对象

    1
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    开启Aspectj生成对象后,会去代码中扫描@aspect注解

  • 配置不同类型的通知
    通知方法上面使用@Before、@AfterReturning、@Around、@AfterThrowing和@After注解,结合切入点表达式配置。
    @after是在方法执行之后执行(有异常也执行),@afterReturning是在返回值之后执行(有异常不执行)。

示例代码

代码结构:

1
2
3
4
5
6
7
8
9
└─src
│ bean.xml

└─com
└─spring5
└─aopanno
Test.java
User.java
UserProxy.java

User类:

1
2
3
4
5
6
7
@Component
public class User {
public void add() {
int a = 10/0;// 测试异常通知
System.out.println("I am add");
}
}

UserProxy类:

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
29
30
31
32
33
34
@Component
@Aspect
public class UserProxy {

@Before(value = "execution(* com.spring5.aopanno.User.add(..))")
public void before() {
System. out .println( "前置通知 before");
}

@AfterReturning(value = "execution(* com.spring5.aopanno.User.add(..))")
public void afterReturning() {
System.out.println("后置通知(返回通知)afterReturning");
}

@After(value = "execution(* com.spring5.aopanno.User.add(..))")
public void after() {
System.out.println("最终通知 after");
}

@AfterThrowing(value = "execution(* com.spring5.aopanno.User.add(..))")
public void afterThrowing() {
System.out.println("异常通知 afterThrowing");
}

@Around(value = "execution(* com.spring5.aopanno.User.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around 环绕之前");

//被增强的方法执行
proceedingJoinPoint.proceed();

System.out.println("Around 环绕之后");
}
}

Test类:

1
2
3
4
5
6
7
8
9
public class Test {

@org.junit.Test
public void testAopAnno(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
User user = context.getBean("user",User.class);
user.add();
}
}

无异常返回结果:
Around 环绕之前
前置通知 before
I am add
Around 环绕之后
最终通知 after
后置通知(返回通知)afterReturning
有异常时返回结果:
Around 环绕之前
前置通知 before
最终通知 after
异常通知 afterThrowing
java.lang.ArithmeticException: / by zero

相同切入点提取

用@Pointcut标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//相同切入点抽取
@Pointcut(value = "execution(* com.spring5.aopanno.User.*(..))")
public void pointTest() {
}

@Before(value = "pointTest()")
public void before() {
System. out .println( "前置通知 before");
}

@AfterReturning(value = "pointTest()")
public void afterReturning() {
System.out.println("后置通知(返回通知)afterReturning");
}
多个增强类对同一个方法进行增强

用@Order注解设置增强类优先级,数字类型值越小表示优先级越高。

1
2
3
4
@Component
@Aspect
@Order(1)
public class PersonProxy{ }
完全注解开发

在启动配置类中添加@EnableAspectJAutoProxy注解:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = { "com.spring5"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ConfigAop {
}

基于配置文件实现

具体步骤
  • 创建增强类和被增强类,创建相关方法
  • 在Spring配置文件中配置两个类对象
  • 在Spring配置文件中配置AOP
示例代码

代码结构如下:

1
2
3
4
5
6
7
8
9
└─src
│ bean.xml

└─com
└─spring5
└─aopxml
Student.java
StudentProxy.java
Test.java

Student类:

1
2
3
4
5
public class Student {
public void buy(){
System.out.println("I want to buy a book...");
}
}

StudentProxy类:

1
2
3
4
5
6
7
8
public class StudentProxy {
public void before(){
System.out.println("I am before...");
}
public void afterReturn(){
System.out.println("I am afterReturn...");
}
}

Test类:

1
2
3
4
5
6
7
8
9
public class Test {

@org.junit.Test
public void testAopXml(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Student student = context.getBean("student",Student.class);
student.buy();
}
}

bean.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="student" class="com.spring5.aopxml.Student"></bean>
<bean id="studentProxy" class="com.spring5.aopxml.StudentProxy"></bean>

<aop:config>
<!--切入点-->
<aop:pointcut id= "p" expression= "execution(* com.spring5.aopxml.Student.buy(..))"/>
<!--配置切面-->
<aop:aspect ref= "studentProxy">
<!--表示把before方法作用到buy方法上,并且做的是前置通知,p是上面的切入点-->
<aop:before method= "before" pointcut-ref= "p"/>
</aop:aspect>
<aop:aspect ref= "studentProxy">
<!--表示把afterReturn方法作用到buy方法上,并且做的是后置通知,p是上面的切入点-->
<aop:after-returning method= "afterReturn" pointcut-ref= "p"/>
</aop:aspect>
</aop:config>
</beans>

运行结果:
I am before…
I want to buy a book…
I am afterReturn…

AspectJ获取方法参数和返回值

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取方法参数
// joinPoint.getArgs() 返回方法参数数组,0、1、2对应第1、2、3个参数
@AfterReturning(value = "execution(* com.xxxImpl.doBusiness(..))")
private void secondProposition(JoinPoint joinPoint){
PropositionApprovalBo bo = (PropositionApprovalBo) joinPoint.getArgs()[1];
}
// 获取方法返回值
@AfterReturning(value = "execution(* com.xxxImpl.addPropositionList(..))",
returning="returnValue")
public void addPropositionList(JoinPoint joinPoint, Object returnValue){
List<PropositionCollect> propositionCollectList = (List<PropositionCollect>) returnValue;
}

只使用返回值时,joinPoint可以省略。

AOP失效场景

1、类内部调用被代理方法,此时被代理方法并不会被增强。
2、被代理方法非public修饰 (未验证,最好改为public)。

示例代码:

1
2
3
4
5
6
7
8
@Service
public class PropositionManageServiceImpl implements PropositionManageService {
public void BatchImport(MultipartFile file, ISessionUser sessionUser) {
this.addPropositionList();
}
public List<PropositionCollect> addPropositionList() {
}
}

由于addPropositionList方法是在PropositionManageServiceImpl类内部调用的,所以AOP代理会失效。不会执行这个切面方法。

1
2
3
4
5
@AfterReturning(value = "execution(* com.xxxImpl.addPropositionList(..))",
returning="returnValue")
public void addPropositionList(JoinPoint joinPoint, Object returnValue){
List<PropositionCollect> propositionCollectList = (List<PropositionCollect>) returnValue;
}

解决方法:使用service调用需要代理的方法。

1
2
3
4
5
6
7
8
9
10
@Service
public class PropositionManageServiceImpl implements PropositionManageService {
@Autowired
private PropositionManageService propositionManageService;
public void BatchImport(MultipartFile file, ISessionUser sessionUser) {
propositionManageService.addPropositionList();
}
public List<PropositionCollect> addPropositionList() {
}
}

Spring AOP切面方法出现异常,会影响主程序运行的解决方法

  • 采用@AfterReturning,在主程序走完后执行
  • try catch住异常代码,并且不要抛出新异常

第二步是必须的。

Spring AOP切面中启用新事务 (可以用@After注解代替)

Spring AOP切面中启用新事务,解决不管主程序是否回滚,都会执行切面方法(比如记录日志)
因为@Transactional也是声明式事务,本身就是AOP实现的,在AOP的代码中使用不起作用。
所以就只能使用spring的编程式事务了,需要引入TransactionTemplate。
示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Autowired
private TransactionTemplate transactionTemplate;

@AfterReturning(value = "execution(* com.rewardinnovation..addAchievementApplication(..))")
public void addAchievement(JoinPoint joinPoint){
log.info("AchievementAspect.addAchievement 开始执行新增成果切面逻辑,joinPoint:{}",joinPoint);
//声明式事务在切面中不起作用,需使用编程式事务
//设置传播行为:总是新启一个事务,如果存在原事务,就挂起原事务
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(new TransactionCallback<T>() {
@Override
public T doInTransaction(TransactionStatus transactionStatus) {
// 具体的切面逻辑都写在这儿
return null;
}
});
}

这种方式只适合于"主程序异常但需要切面正常执行"的场景。完全可以使用@After注解代替
参考链接