target
掌握AOP概念和相关术语
会编写基于XML的AOP编程和基于注解的AOP编程
理解MethodBeforeAdvice接口和execution表达式的使用
理解AOP底层原理
了解AOP中的一个"坑"
1. AOP概述
AOP (Aspect Oriented Programing) ⾯向切⾯编程 ,aop编程本质上就是Spring动态代理开发
以切⾯为基本单位的程序开发,通过切⾯间的彼此协同,相互调⽤,完成程序的构建
切⾯ = 切⼊点 + 额外功能
OOP (Object Oritened Programing) ⾯向对象编程 ,比如:Java
以对象为基本单位的程序开发,通过对象间的彼此协同,相互调⽤,完成程序的构建
POP (Producer Oriented Programing) ⾯向过程(⽅法、函数)编程 ,比如:C语言
以过程为基本单位的程序开发,通过过程间的彼此协同,相互调⽤,完成程序的构建
举个例子来说明一下:
如果有一个UserService类,里面有这样三个方法:addUser()、updateUser()、deleteUser()。现在希望给这三个方法都加上事务,但是不能修改这个类的源码。所以我们想到写一个子类去继承他,然后修改子类就好了。
SonUserService类继承UserService类,子类里可以直接调用父类的方法。比如:addUser()方法我就可以先调用父类的addUser()方法:super.addUser()。然后在前面开启事务,在后面提交事务。
我们发现,后面两个方法依次都要这么做,而且开启事务、提交事务不断重复出现。
这是我们以前写的东西,我们称之为纵向继承。这就是传统的oop思想。
现在有一个新的解决方案:
既然开启事务和提交事务是重复代码,可以把它抽取出来放在一个类里面。
写一个A类,before()方法里面写开启事务,after()方法里面写提交事务。我希望A类的方法都要作用到UserService类的相应方法的前后去,此时就可以用到代理。写一个代理类,把这两个无关的类建立起关系,就像房产中介就是代理类,他把买房子的和买房子的建立起关系。代理类把左边的代码拿过来,把右边的代码也拿过来,然后组合在一起。所以代理类这样写:
A.before();
UserService.addUser();
A.after();
有人可能说,你这么写完和刚才有啥区别?不是一样了吗?
不一样的。Spring提供了动态代理,代理类其实是Spring去做的,我们只需要将A类写好,UserService类写好,然后都交给Spring去做了,这样我们做的事就仅仅是业务逻辑了,该有的功能都有,但是代码量却少了。这就说我们即将学习的AOP。
AOP有如下特点和应用:
经典应用:性能检测、事务管理、安全检查、缓存。
Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。
1.1 AOP定义
AOP的概念: 本质就是Spring的动态代理开发,通过代理类为原始类增加额外功能。 好处: 利于原始类的维护,减少代码量,使开发人员专注于业务逻辑的开发。 注意: AOP编程不可能取代OOP,OOP编程有益补充。
1.2 AOP术语
target:目标类,需要被代理的类。也就是上图的UserService类。
JointPoint:连接点,指可能被拦截到的方法。例如:所有的方法。
-
PointCut:切入点,已经被增强的方法。例如:addUser()
怎么记连接点和切入点:
比如:洗手间所有的马桶就是连接点,正在被使用的马桶就是切入点
advice:通知/增强,增强的代码。例如:before()、after()
Weaving:织入,把增强advice应用到目标对象target来创建新的代理对象Proxy的过程。
Proxy:代理类
aspect:切面,切入点和通知的结合。
1.3 4种advice
如果要对一个切入点进行增强,首先考虑的应该是在切入点的什么位置进行增强,是在切入点前面增强,还是后面,还是前后一起?
通知类型 | 需要实现的接口 | 接口中的方法 | 执行时机 |
---|---|---|---|
前置通知 | org.springframework.aop.MethodBeforeAdvice | before() | 目标方法之前 |
后置通知 | org.springframework.aop.AfterReturningAdvice | afterReturning() | 目标方法执行后 |
异常通知 | org.springframework.aop.ThrowsAdvice | 无 | 目标方法发生异常时 |
环绕通知 | org.aopalliance.intercept.MethodInterceptor | invoke() | 调用目标方法的整个过程 |
2. 基于xml的AOP编程
新建Java项目:Spring-06
2.1 前置通知
前置通知需要实现MethodBeforeAdvice接口。
① 代码实现
第一步:导入jar
aopalliance.jar
aspectjweaver.jar
commons-logging-1.2.jar
spring-aop-4.3.9.RELEASE.jar
spring-beans-4.3.9.RELEASE.jar
spring-context-4.3.9.RELEASE.jar
spring-core-4.3.9.RELEASE.jar
spring-expression-4.3.9.RELEASE.jar
注意:必须有 aopalliance 和 aspectjweaver 的支持,否则 aop功能无法实现。
第二步:编写目标类
- UserService.java
package com.lee.spring.service;
public interface UserService {
void addUser();
void updateUser();
boolean deleteUser(int no);
}
- UserServiceImpl.java
package com.lee.spring.service.impl;
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("addUser...");
}
@Override
public void updateUser() {
System.out.println("updateUser...");
}
@Override
public boolean deleteUser(int no) {
System.out.println("deleteUser...");
return true;
}
}
第三步:编写通知类BeforeAdvice.java
package com.lee.spring.aop;
public class BeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("前置通知执行了。。。。");
}
}
第四步:编写配置文件
<?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-4.3.xsd">
<!-- 将目标类纳入到IOC容器 -->
<bean id="userService" class="com.lee.spring.service.impl.UserServiceImpl"></bean>
<!-- 将通知类纳入到IOC容器 -->
<bean id="beforeAdvice" class="com.lee.spring.aop.BeforeAdvice"></bean>
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(void com.lee.spring.service.impl.UserServiceImpl.addUser())" id="pointcut"/>
<!-- 将切入点和通知连接起来 -->
<!-- advisor就是顾问的意思,顾问就是给两个点起一个桥梁作用 -->
<aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
</aop:config>
</beans>
第五步:测试
@Test
public void test01() {
//1.加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 获取bean
UserService userService = (UserService)context.getBean("userService");
userService.addUser();
userService.deleteUser(1);
userService.updateUser();
}
输出:
前置通知执行了。。。。
addUser...
deleteUser...
updateUser...
可以通过 debug的方式来查看 addUser()方法、deleteUser()、updateUser()方法:
Debug As 进行启动:
可以发现,userService对象使用的就是代理对象。
扩展:
如果我们想给delete方法也配置一个前置通知,直接修改配置文件就可以:
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(void com.lee.spring.service.UserServiceImpl.impl.addUser()) or execution( boolean com.lee.spring.service.UserServiceImpl.impl.deleteUser(int))" id="pointcut"/>
<!-- 将切入点和通知连接起来 -->
<!-- advisor就是顾问的意思,顾问就是给两个点起一个桥梁作用 -->
<aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
</aop:config>
② MethodBeforeAdvice详解
MethodBeforeAdvice接⼝作⽤:额外功能运⾏在原始⽅法执⾏之前,进⾏额外功能操作。
关于前置通知类BeforeAdvice中before()方法中的参数解释
public class BeforeAdvice implements MethodBeforeAdvice {
/**
Method: 额外功能所增加给的那个原始⽅法
比如:login⽅法、register⽅法、showOrder⽅法
Object[]: 额外功能所增加给的那个原始⽅法的参数。
比如:String name,String password、User
Object: 额外功能所增加给的那个原始对象
比如:UserServiceImpl、OrderServiceImpl
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("method名字:" + method.getName());
System.out.println("method参数个数:" + method.getParameterCount());
System.out.println( "args:" + Arrays.toString(args));
System.out.println("target:" + target);
System.out.println("前置通知执行了。。。。");
}
}
输出:
method名字:deleteUser
method参数个数:1
method的返回值类型:boolean
args:[1]
target:com.lee.service.UserServiceImpl@6ab7a896
前置通知执行了。。。。
Method: 切入点(目标方法)
Object[] args:原始方法的参数
Object target:原始对象或目标对象(UserServiceImpl对象)
③ execution详解
例: execution (* com.sample.service..*.*(..))
整个表达式可以分为五个部分:
1、execution():表达式主体。
2、第一个*号:表示返回类型, *号表示所有的类型。
3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service包、子孙包下所有类的方法。
4、第二个*号:表示类名,*号表示所有的类。
5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
🌰:
-
boolean addStudent(com.lee.entity.Student)
返回值类型为boolean,参数类型为com.lee.entity.Student的所有叫addStudent()方法。
-
boolean com.lee.service.StudentService.addStudent(com.lee.entity.Student)
返回值为boolean,参数类型为com.lee.entity.Student,在com.lee.service.StudentService类下的addStudent方法。
-
* addStudent(com.lee.entity.Student)
返回值任意,参数类型为com.lee.entity.Student的所有叫addStudent()方法。
-
void *(com.lee.entity.Student)
返回值为void,参数类型为com.lee.entity.Student的任意方法。
-
* com.lee.service.*.*(..)
返回值任意,com.lee.service包下的所有类下的所有方法,参数类型任意。(不包含子包)
-
* com.lee.service..*.*(..)
返回值任意,com.lee.service包下的所有类下的所有方法,参数类型任意。(包含子包)
2.2 后置通知
后置通知需要实现AfterReturningAdvice接口。
第一步:导入jar
aopalliance.jar
aspectjweaver.jar
第二步:编写目标类
- UserService.java
public interface UserService {
void addUser();
void updateUser();
boolean deleteUser(int no);
}
- UserServiceImpl.java
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("addUser...");
}
@Override
public void updateUser() {
System.out.println("updateUser...");
}
@Override
public boolean deleteUser(int no) {
System.out.println("deleteUser...");
return true;
}
}
第三步:编写通知类AfterAdvice.java
public class AfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("返回值:" + returnValue);
System.out.println("method方法名:" + method.getName());
System.out.println("method参数个数:" + method.getParameterCount());
System.out.println("method返回值类型:" + method.getReturnType());
System.out.println( "目标对象:" +target);
System.out.println("后置通知执行。。。");
}
}
第四步:编写配置文件
<?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:p="http://www.springframework.org/schema/p"
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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 将目标类纳入到IOC容器 -->
<bean id="userService" class="com.lee.service.UserServiceImpl"></bean>
<!-- 将通知类纳入到IOC容器 -->
<bean id="afterAdvice" class="com.lee.aop.AfterAdvice"></bean>
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点(目标方法) -->
<aop:pointcut expression="execution(* com.lee.spring.service.UserServiceImpl.impl.updateUser()) or execution(* com.lee.spring.service.UserServiceImpl.impl.deleteUser(int))" id="pointcut"/>
<!-- 配置顾问(将目标方法和通知连接) -->
<aop:advisor advice-ref="afterAdvice" pointcut-ref="pointcut"/>
</aop:config>
</beans>
第五步:测试
@Test
public void test02() {
//1.加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 获取bean
UserService userService = (UserService)context.getBean("userService");
userService.addUser();
System.out.println("-----------");
userService.deleteUser(1);
System.out.println("-------------");
userService.updateUser();
}
输出:
addUser...
-----------
deleteUser...
返回值:true
method方法名:deleteUser
method参数个数:1
method返回值类型:boolean
目标对象:com.lee.service.UserServiceImpl@fdefd3f
后置通知执行。。。
-------------
updateUser...
返回值:null
method方法名:updateUser
method参数个数:0
method返回值类型:void
目标对象:com.lee.service.UserServiceImpl@fdefd3f
后置通知执行。。。
2.3 异常通知
异常通知需要实现ThrowsAdvice接口。
第一步:导入jar
aopalliance.jar
aspectjweaver.jar
第二步:编写目标类
- UserService.java
public interface UserService {
void addUser();
void updateUser();
boolean deleteUser(int no);
}
- UserServiceImpl.java
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("addUser...");
}
@Override
public void updateUser() {
System.out.println("updateUser...");
}
@Override
public boolean deleteUser(int no) {
System.out.println("deleteUser...");
return true;
}
}
第三步:编写通知类
public class ExceptionAdvice implements ThrowsAdvice {
public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
System.out.println("method方法名:" + method.getName());
System.out.println("method方法参数个数:" + method.getParameterCount());
System.out.println("method方法默认值:" + method.getDefaultValue());
System.out.println("args" +Arrays.toString( args));
System.out.println("目标对象:" + target);
System.out.println("异常信息:" + ex.getMessage());
}
}
我们发现ThrowsAdvice接口中没有声明方法,但是文档中有提示,告诉我们必须写一个这样的方法:
第四步:编写配置文件
<?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:p="http://www.springframework.org/schema/p"
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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 将目标类纳入到IOC容器 -->
<bean id="userService" class="com.lee.service.UserServiceImpl"></bean>
<!-- 将通知类纳入到IOC容器 -->
<bean id="excetionAdvice" class="com.lee.aop.ExceptionAdvice"></bean>
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点(目标方法) -->
<aop:pointcut expression="execution(* com.lee.service.UserServiceImpl.updateUser()) or execution(* com.lee.service.UserServiceImpl.deleteUser(int))" id="pointcut"/>
<!-- 配置顾问(将目标方法和通知连接) -->
<aop:advisor advice-ref="excetionAdvice" pointcut-ref="pointcut"/>
</aop:config>
</beans>
我们将updateUser和deleteUser方法配置了增强。
第五步:测试
@Test
public void testBeforeAdvice() {
//1.加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 获取bean
UserService userService = (UserService)context.getBean("userService");
userService.addUser();
System.out.println("-----------");
userService.deleteUser(1);
System.out.println("-------------");
userService.updateUser();
}
输出:
addUser...
-----------
deleteUser...
-------------
updateUser...
发现,增强没有起作用。这是由于切入点(目标方法)没有异常,所以异常通知不会触发。
修改目标方法:给UserServiceImpl类中deleteUser()方法制造一个异常
@Override
public boolean deleteUser(int no) {
int i = 1/0;
System.out.println("deleteUser...");
return true;
}
输出:
addUser...
-----------
method方法名:deleteUser
method方法参数个数:1
method方法默认值:null
args[1]
目标对象:com.lee.service.UserServiceImpl@11dc3715
异常信息:/ by zero
发现,异常通知触发,而且程序也抛出了异常,并且终止在发生异常的代码处,后面的代码不会继续执行。
2.4 环绕通知
看名字就知道环绕通知是环绕着目标方法的,既然是环绕,所有他能实现前置通知、后置通知和异常通知。
环绕通知需要实现MethodInterceptor接口。
环绕通知的本质是拦截器。
下面看一下案例实现:
第一步:导入jar
aopalliance.jar
aspectjweaver.jar
第二步:编写目标类
- UserService.java
public interface UserService {
void addUser();
void updateUser();
boolean deleteUser(int no);
}
- UserServiceImpl.java
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("addUser...");
}
@Override
public void updateUser() {
System.out.println("updateUser...");
}
@Override
public boolean deleteUser(int no) {
System.out.println("deleteUser...");
return true;
}
}
第三步:编写通知类RoundAdvice.java
public class RoundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
/*
* invocation里包含着目标方法的全部信息
* 包括:方法名、参数个数、参数值、返回值。。。。
* 甚至可以控制目标方法是否执行
*/
Object result = null;
try {
//1.invocation.proceed()之前的代码是前置通知
System.out.println("环绕通知实现的前置通知。。。");
System.out.println("目标方法名:" + invocation.getMethod().getName());
result = invocation.proceed();//控制目标方法的执行
//2.invocation.proceed()之后的代码是后置通知
System.out.println("环绕通知实现的后置通知。。。");
System.out.println("目标对象:" + invocation.getThis());
System.out.println("返回值:" + result);
}catch (Exception e) {
//3.异常通知
System.out.println("环绕通知实现的异常通知。。。");
}
/*
* 返回值是目标方法的返回值
* 由于环绕通知拥有目标方法的绝对控制权,他甚至可以偷梁换柱:将目标方法的返回值进行更改
*/
return result;
}
}
第四步:编写配置文件
<?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:p="http://www.springframework.org/schema/p"
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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 将目标类纳入到IOC容器 -->
<bean id="userService" class="com.lee.service.UserServiceImpl"></bean>
<!-- 将通知类纳入到IOC容器 -->
<bean id="roundAdvice" class="com.lee.aop.RoundAdvice"></bean>
<!-- 配置aop -->
<aop:config>
<!-- 配置切入点(目标方法) -->
<aop:pointcut expression="execution(* com.lee.service.UserServiceImpl.deleteUser(int))" id="pointcut"/>
<!-- 配置顾问(将目标方法和通知连接) -->
<aop:advisor advice-ref="roundAdvice" pointcut-ref="pointcut"/>
</aop:config>
</beans>
第五步:测试
@Test
public void test04() {
//1.加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 获取bean
UserService userService = (UserService)context.getBean("userService");
userService.addUser();
System.out.println("-----------");
userService.deleteUser(1);
System.out.println("-------------");
userService.updateUser();
}
输出:
addUser...
-----------
环绕通知实现的前置通知。。。
目标方法名:deleteUser
deleteUser...
环绕通知实现的后置通知。。。
目标对象:com.lee.service.UserServiceImpl@1a0dcaa
返回值:true
-------------
updateUser...
如果想查看环绕通知实现的异常通知,将目标方法设计一个异常就可以了。
3. 基于注解的aop
将一个类变成有特定功能的类,有四种做法:
继承类
实现接口
注解
配置文件
下面我们说一下用注解方式实现的的aop:
3.1 前置通知
① 实现步骤
第一步:导入jar包
- aspectjweaver.jar
第二步:编写业务类
- UserService.java
public interface UserService {
void addUser(User student);
void updateUser(User student);
boolean deleteUser(int no);
}
- UserServiceImpl
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void addUser(User user) {
System.out.println("addUser..." + user);
}
@Override
public void updateUser(User user) {
System.out.println("updateUser..." + user);
}
@Override
public boolean deleteUser(int no) {
System.out.println("deleteUser...学号是:" + no );
return true;
}
}
第三步:编写通知类AnnotationAdvice.java
@Component
@Aspect
public class AnnotationAdvice {
@Before("execution(* com.lee.service.*.addUser(..))")
public void before() {
System.out.println("前置通知执行。。。");
}
}
在类上加注解@Aspect,就表示把普通的类变成了通知类。
在方法上加注解@Before,就表示把普通的方法变成了前置方法。@Before里面需要些切入点的表达式。
第四步:编写配置文件
<?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:p="http://www.springframework.org/schema/p"
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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 开启包扫描 -->
<context:component-scan base-package="com.lee"></context:component-scan>
<!-- 开启aop支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
如果要用注解开发aop,配置文件里必须开启对aop的支持。
第五步:测试
@Test
public void testBeforeAdvice() {
//1.加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//2. 获取bean
UserService userService = (UserService)context.getBean("userService");
User user = new User(1001, "zs");
userService.addUser(user);
System.out.println("-----------");
userService.deleteUser(1);
System.out.println("-------------");
userService.updateUser(user);
}
输出:
前置通知执行。。。
addUser...User [no=1001, name=zs]
-----------
deleteUser...学号是:1
-------------
updateUser...User [no=1001, name=zs]
② 注意事项
普通的类要变成通知类,需要在类上加注解@Aspect
通知类里面的方法要变成前置通知方法,需要在方法上@Before注解,而且注解里面需要配置切入点表达式。
如果想让注解配置的aop生效,需要在配置文件中开启对aop的支持。
此案例我们将目标类和通知类加入到IOC容器的方式是:注解扫描方式。也可以用xml配置方式。
③ JoinPoint
怎么可以像实现接口那样,获取到一些关于切入点的相关信息呢?用JoinPoint。
注意是org.aspectj.lang.JoinPoint。
通知类:
@Component
@Aspect
public class AnnotationAdvice {
@Before("execution(* com.lee.service.*.addUser(..))")
public void before(JoinPoint jp) {
System.out.println("目标方法相关信息:" + jp.getSignature());
System.out.println("目标方法名:" + jp.getSignature().getName());
System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
System.out.println("目标对象:" + jp.getThis());
System.out.println("前置通知执行。。。");
}
}
输出:
目标方法签名:void com.lee.service.UserService.addUser(User)
目标方法名:addUser
方法参数:[User [no=1001, name=zs]]
目标对象:com.lee.service.UserServiceImpl@7c7b252e
前置通知执行。。。
addUser...User [no=1001, name=zs]
-----------
deleteUser...学号是:1
-------------
updateUser...User [no=1001, name=zs]
④ 相关方法
方法 | 解释 |
---|---|
getSignature() | 获取目标方法相关信息 |
getSignature().getName() | 获取目标方法名 |
getArgs() | 获取目标方法参数 |
getThis() | 获取目标对象 |
getTarget() | 获取目标对象 |
3.2 后置通知
后置通知的注解是@AfterReturning
@Component
@Aspect
public class AnnotationAdvice {
@AfterReturning( pointcut = "execution(* com.lee.service.*.deleteUser(..))" , returning = "returnVal")
public void after(JoinPoint jp , Object returnVal) {
System.out.println("目标方法相关信息:" + jp.getSignature());
System.out.println("目标方法名:" + jp.getSignature().getName());
System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
System.out.println("目标对象:" + jp.getThis());
System.out.println("返回值:" + returnVal);
System.out.println("后置通知执行。。。");
}
}
注意:
如果想查看返回值的回话,需要在@AfterReturning里声明returning = "xxx"。
@AfterReturning( pointcut = "execution(* com.lee.service.*.deleteUser(..))" , returning = "returnVal")中,既可以用 pointcut = "xxx",也可以用 value = "xxx"。
3.3 异常通知
异常通知用注解@AfterThrowing。
如果需要打印异常信息需要在@AfterThrowing里加一个throwing = "xxx"
@Component
@Aspect
public class AnnotationAdvice {
@AfterThrowing(pointcut = "execution(* com.lee.service.*.addUser(..))" , throwing = "th")
public void myException(JoinPoint jp , Exception th) {
System.out.println("目标方法相关信息:" + jp.getSignature());
System.out.println("目标方法名:" + jp.getSignature().getName());
System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
System.out.println("目标对象:" + jp.getThis());
System.out.println("异常信息:" + th.getMessage());
System.out.println("异常通知执行。。。");
}
}
输出:
目标方法相关信息:void com.lee.service.UserService.addUser(User)
目标方法名:addUser
方法参数:[User [no=1001, name=zs]]
目标对象:com.lee.service.UserServiceImpl@48f2bd5b
异常信息:/ by zero
异常通知执行。。。
扩展:
上面方法捕获的异常级别是Exception,如果我只想捕获特定异常,比如:ArithmeticException。当程序产生其他异常时,就会捕捉不到。
@Aspect
public class AnnotationAdvice {
@AfterThrowing(pointcut = "execution(* com.lee.service.*.addUser(..))" , throwing = "th")
public void myException(JoinPoint jp , NullPointerException th) {
System.out.println("目标方法相关信息:" + jp.getSignature());
System.out.println("目标方法名:" + jp.getSignature().getName());
System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
System.out.println("目标对象:" + jp.getThis());
System.out.println("异常信息:" + th.getMessage());
System.out.println("异常通知执行。。。");
}
}
异常信息就没有被捕获到,是由于程序中异常通知仅仅捕获的是NullPointerException,而目标方法里面出现的异常时ArithmeticException,所以没有捕获到
3.4 最终通知
最终通知是无论程序正常执行还是异常执行,都会执行的一个通知。类似于try、catch、finally里面的finally。
最终通知用的注解是@After。
@Component
@Aspect
public class AnnotationAdvice {
@After("execution(* com.lee.service.*.addUser(..))" )
public void zuizhong() {
System.out.println("最终通知执行。。。");
}
}
3.5 环绕通知
环绕通知用的注解是@Around
@Component
@Aspect
public class AnnotationAdvice {
@Around(value = "execution(* com.lee.service.*.addUser(..))")
//获取目标对象的详细信息不能再用JoinPoint,需要用子接口ProceedingJoinPoint。
public void around(ProceedingJoinPoint jp) {
try {
//执行目标方法之前,前置通知
System.out.println("前置通知执行。。。");
jp.proceed();//执行目标方法
//执行目标方法之后,后置通知
System.out.println("后置通知执行。。。");
}catch (Throwable e) {
System.out.println("异常通知执行。。。");
}finally {
System.out.println("最终通知执行。。。");
}
}
}
目标方法没有异常,输出:
前置通知执行。。。
addUser...User [no=1001, name=zs]
后置通知执行。。。
最终通知执行。。。
目标方法存在异常,输出:
前置通知执行。。。
addUser...User [no=1001, name=zs]
异常通知执行。。。
最终通知执行。。。
执行顺序
-
目标方法没有异常:
前置通知-->目标方法-->后置通知-->最终通知
-
目标方法存在异常:
前置通知-->目标方法-->异常通知-->最终通知
注意事项
获取目标对象的详细信息不能再用JoinPoint,需要用子接口ProceedingJoinPoint。
catch块里异常信息的捕获级别必须是Throwable,不能是其他。
如果目标方法没有返回值,则环绕通知方法的返回值也可以是void。如果目标方法有返回值,则环绕通知方法也必须有返回值,否则会报错:Null return value from advice does not match primitive return type for: ....
@Aspect
public class AnnotationAdvice {
@Around(value = "execution(* com.lee.service.*.deleteUser(..))")
public Object around(ProceedingJoinPoint jp) {
Object result = null ;
try {
//执行目标方法之前,前置通知
System.out.println("前置通知执行。。。");
result = jp.proceed();//执行目标方法
//执行目标方法之后,后置通知
System.out.println("后置通知执行。。。");
}catch (Throwable e) {
System.out.println("异常通知执行。。。");
}finally {
System.out.println("最终通知执行。。。");
}
return result;
}
}
4. AOP底层实现原理
AOP的底层原理实现就是Spring的动态代理。
4.1 Spring创建的动态代理类在哪⾥
Spring框架在运⾏时,通过动态字节码技术,在JVM中创建的,运⾏在JVM内部,等程序结束后,会和 JVM ⼀起消失。
-
什么叫动态字节码技术:
动态字节码技术
JVM是怎么创建对象的?
首先需要编写 .java 的源文件,源文件编译后会变成 .class的字节码文件,JVM通过加载字节码就能创建出这个类的对象。
那什么是动态字节码?
动态字节码就意味着不需要编写 .java的源文件,也就不能编译为 .class文件。但是虚拟机创建对象肯定需要的还是 .class格式的字节码,此时的字节码就是动态创建的。
动态字节码是怎么创建的?
通过第三方的动态字节码框架创建,常见的动态字节码框架有 ASM、Javaassist、Cglib。这些框架可以动态的生成字节码。
所以,动态代理类的本质就是动态字节码技术,比如 UserServiceProxy就会自动的通过动态字节码在JVM中生成,而不需要去手动编写。
-
结论:
动态代理不需要定义类⽂件,都是JVM运⾏过程中动态创建的,所以不会造成静态代理,类⽂件数量过多,影响项⽬管理的问题。
4.2 Spring工厂如何加工原始对象
- 思路分析:
就是使用后置bean去对原始对象进行再加工,再加工阶段加上额外功能。
再加工的过程使用的是动态代理。
- 核心编码
UserService.java接口:
package com.lee.factory;
public interface UserService {
void register();
void login();
}
UserServiceImpl.java 实现类:
package com.lee.factory;
public class UserServiceImpl implements UserService {
@Override
public void register() {
System.out.println("注册。。。。。");
}
@Override
public void login() {
System.out.println("登录。。。。");
}
}
将UserServiceImpl纳入到IoC容器:
<bean id="userService" class="com.lee.factory.UserServiceImpl"></bean>
创建ProxyBeanPostProcessor.java,实现代理:
package com.lee.factory;
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String args) throws BeansException {
ClassLoader loader = ProxyBeanPostProcessor.class.getClassLoader();
Class<?>[] interfaces = bean.getClass().getInterfaces();
InvocationHandler invocationHandle = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---------log.....-----");
Object invoke = method.invoke(bean, args);
return invoke;
}
};
//使用JDK代理创建对象
Object ret = Proxy.newProxyInstance(loader, interfaces, invocationHandle);
return ret;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String args) throws BeansException {
return bean;
}
}
测试:
public class TestAopProxy {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext-factory.xml");
UserService userService = (UserService)context.getBean("userService");
userService.register();
userService.login();
}
}
输出:
---------log.....-----
注册。。。。。
---------log.....-----
登录。。。。
5. AOP开发中的⼀个坑
先看一个案例:
业务里有两个方法,分别是a() 和 b():
package com.lee.spring.service.impl;
public class UserServiceImpl {
public void a() {
System.out.println("a方法执行");
}
public void b() {
System.out.println("b方法执行");
}
}
有一个额外方法,通过 Spring 进行增强:
package com.lee.spring.aop;
public class Before implements MethodBeforeAdvice {
@Override
public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
System.out.println("前置增强执行");
}
}
配置文件:
<?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-4.3.xsd">
<!-- 将UserService纳入到IOC容器 -->
<bean id = "userService" class="com.lee.spring.service.impl.UserServiceImpl"></bean>
<!-- 将Before增强纳入到IOC容器 -->
<bean id = "before" class="com.lee.spring.aop.Before"></bean>
<!-- 配置增强 -->
<aop:config>
<aop:pointcut expression="execution(* *..UserServiceImpl.*(..))" id="pointcut"/>
<aop:advisor advice-ref="before" pointcut-ref="pointcut"/>
</aop:config>
</beans>
编写测试类进行测试:
@Test
public void test01() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServiceImpl userService = (UserServiceImpl)context.getBean("userService");
userService.a();
userService.b();
}
控制台输出:
前置增强执行
a方法执行
前置增强执行
b方法执行
以上操作都是正常的 aop 前置增强,如果业务逻辑变为:
package com.lee.spring.service.impl;
public class UserServiceImpl {
public void a() {
System.out.println("a方法执行");
b();
}
public void b() {
System.out.println("b方法执行");
}
}
测试变为:
@Test
public void test01() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServiceImpl userService = (UserServiceImpl)context.getBean("userService");
userService.a();
}
控制台输出:
前置增强执行
a方法执行
b方法执行
很明显,在方法a() 中 调用b()方法,b()方法的前置增强就不生效了。这就是aop中的一个坑。
原因分析:
在执行 userService.a();
时,a()方法调用的时候userService对象
,而userService对象
是从ioc容器中获取的代理对象,所以可以进行增强。但是在 a() 方法中调用 b() 方法,b() 方法是来自与 userService对象
本身,而不是 Spring代理的,所以不具备增强功能。
所以可以这样改进:
package com.lee.spring.service.impl;
public class UserServiceImpl {
public void a() {
System.out.println("a方法执行");
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServiceImpl userService = (UserServiceImpl)context.getBean("userService");
userService.b();
}
public void b() {
System.out.println("b方法执行");
}
}
在调用 b() 方法的时候,不是直接调用,而是从ioc中获取代理对象,然后调用代理对象的 b() 方法。
但是这样有个问题:Spring工厂是重量级资源,会侵占内存,所以一个应用中只创建一个工厂就足够了。
可以这样更改:
package com.lee.spring.service.impl;
public class UserServiceImpl implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
public void a() {
System.out.println("a方法执行");
UserServiceImpl userService = (UserServiceImpl)context.getBean("userService");
userService.b();
}
public void b() {
System.out.println("b方法执行");
}
}