一、Spring AOP简介
1、什么是AOP
面向切面编程(也称面向方面编程)。是面向对象编程的一种补充。在传统业务处理代码中,通常会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但是要实现某个功能(如日志记录),同样的代码荏苒会分散到各个方法中。这样,如果关闭某个功能,或者对其进行修改,就必须要修改所有的方法。为解决这一个为题,AOP采用横向抽取机制,将分散在各个方法中的重复代码提取出来,然后再程序编译或运行时,在将这些提取出来的代码应用到需要执行的地方。它只是OOP的眼神和补充。
AOP的使用,使开发人员专注核心业务,而不用过多的关注其他业务的实现。增强代码可维护性。目前流行的AOP框架有两个,分别为Spring AOP 和 AspectJ。Spring AOP使用纯Java代码实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。AspectJ基于Java语言的AOP框架,在编译时提供横向代码的织入。
底层原理:使用动态代理实现
- 第一种情况:有接口情况,使用动态代理创建接口实现类代理对象
- 第二种情况:无接口情况,使用动态代理创建的子类代理对象
AOP原理:
来源:传智播客
2、AOP术语
- Aspect(切面):切入点和通知的结合,把增强应用到具体方法上的过程称为切面。通常指封装的用于横向插入系统功能(如事务、日志等)的类。
- JointPoint(连接点):在程序执行过程中的某个阶段点,指的是方法的调用。类里面可以被增强的方法。
- Pointcut(切入点):指漆面与程序流程的交叉点,那些需要处理的连接点。实际增强的方法称为切入点。
- Advice(增强/通知处理):在定义好的切入点要执行的程序代码。实际增强的逻辑,比如:扩展日志功能。
- Target(目标对象):代理的目标对象(要增强的类)。
- Proxy(代理):被增强后产生的对象。
- Weaving(织入):把增强应用到目标的过程。
二、动态代理
Spring中的AOP代理,可以是JDK动态代理,也可以是CGLIB代理。
1、JDK动态代理
JDK动态代理是通过java.lang.reflect.Proxy类来实现的,我们可以调用Proxy类的newProxyInstance()方法来创建代理对象。创建项目,同时创建jdk包,在该包下创建接口UserDao,编写添加和删除的方法:
public interface UserDao {
public void addUser();
public void deleteUser();
}
同时在该报下创建UserDao接口的实现类UserDaoImpl:
/**
* 目标类
* @author Youguangfu
*
*/
@Repository("user")
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
// TODO Auto-generated method stub
System.out.println("添加用户。。。");
}
@Override
public void deleteUser() {
// TODO Auto-generated method stub
System.out.println("删除用户。。。");
}
}
将实现类作为目标类,对其中的方法进行增强处理。
创建aspect包,在该包下创建切面类MyAspect,并定义一个模拟权限检查和模拟记录日志的方法,这两个方法表示切面中的通知:
/**
* 切面类:可以存在多个通知 Advice(即增强的方法)
* @author Youguangfu
*
*/
public class MyAspect {
public void check_Permissions() {
System.out.println("模拟检查权限。。。");
}
public void log() {
System.out.println("模拟记录日志。。。");
}
}
在jdk包下创建代理类JdkProxy ,该类需要实现InvocationHandler接口,并编写代理方法:
/**
* JDK代理类
* @author Youguangfu
*
*/
public class JdkProxy implements InvocationHandler {
//声明目标类接口
private UserDao userDao;
//创建代理方法
public Object createProxy(UserDao userDao) {
this.userDao = userDao;
//类加载器
ClassLoader classLoader = JdkProxy.class.getClassLoader();
//被代理对象实现所有接口
Class[] clazz = userDao.getClass().getInterfaces();
//使用代理类,进行增强,返回的是代理后的对象
return Proxy.newProxyInstance(classLoader, clazz, this);
}
/**
* 所有动态代理类的方法调用,都会交由invoke()方法去处理
* proxy被代理后的对象
* method将要被执行的方法信息(反射)
* args执行方法时需要的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// TODO Auto-generated method stub
System.out.println("调用");
//声明切面
MyAspect myAspect = new MyAspect();
//前增强
myAspect.check_Permissions();
//在目标类上调用方法,并传入参数
//当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用。
/**
* 因为JDK生成的最终真正的代理类,它继承自Proxy并实现了我们定义的Subject接口,在实现Subject接口方法的内部,通过反射调用了
InvocationHandlerImpl的invoke方法。
*/
System.out.println("method"+method);
Object obj = method.invoke(userDao, args);
//后增强
myAspect.log();
return obj;
}
}
该类实现了InvocationHandler接口,并实现了接口中的invoke()方法,所有动态代理类所调用的方法都由该方法处理。创建测试类:
public class JdkTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建代理对象
JdkProxy jdkProxy = new JdkProxy();
//创建目标对象
UserDao userDao = new UserDaoImpl();
//从代理对象中获取增强后的对象
UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao);
//执行方法
System.out.println("未增强");
userDao.addUser();
userDao.deleteUser();
//当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用。
/**
* 因为JDK生成的最终真正的代理类,它继承自Proxy并实现了我们定义的Subject接口,在实现Subject接口方法的内部,通过反射调用了
InvocationHandlerImpl的invoke方法。
*/
System.out.println("增强后。。。。。。。。。。。。。。。。。。。。。。。");
userDao1.addUser();
userDao1.deleteUser();
}
}
2、CGLIB代理
就打开动态代理虽然简单,但是有局限性—使用动态代理的对象必须实现一个或多个接口。CGLIB是一个高性能开源的代码生成包,采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。在上一个项目中创建cglib包,同时创建目标类UserDao:
public class UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
创建代理类CglibProxy,该类需要实现MethodInterceptor接口:
/**
* 代理类
* @author Youguangfu
*
*/
public class CglibProxy implements MethodInterceptor {
//代理方法
public Object createProxy(Object target) {
//创建一个动态类对象
Enhancer enhancer = new Enhancer();
//确定需要增强的类,确定其父类
enhancer.setSuperclass(target.getClass());
//添加回调函数
enhancer.setCallback(this);
//返回创建的代理类
return enhancer.create();
}
/**
* proxy GCLib根据指定的父类生成的代理对象
* method拦截的方法
* args 拦截方法的参数数组
* methodProxy方法的代理对象,用于执行父类的方法
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// TODO Auto-generated method stub
//创建切面类对象
MyAspect myAspect = new MyAspect();
//前增强
myAspect.check_Permissions();
//目标方法执行
Object obj = methodProxy.invokeSuper(proxy, args);
//后增强
myAspect.log();
return obj;
}
}
首先创建一个动态类对象Enhancer,它是CGLIB的核心类;然后调用Enhancer类的setSuperclass方法来确定目标对象;接下来调用setCallback()方法添加回调函数,其中this表示代理类CglibProy本身;最后通过renturn语句将穿件的代理类对象返回。创建此时雷,在main方法中首先创建代理对象和目标对象,然后从代理对象中获得增强后的目标对象,最后嗲用对象的方法:
public class CGLibTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建代理对象
CglibProxy cglibProxy = new CglibProxy();
//创建目标对象
UserDao userDao = new UserDao();
//获取增强后的目标对象
UserDao userDao1 = (UserDao) cglibProxy.createProxy(userDao);
//执行方法
System.out.println("未代理");
userDao.addUser();
userDao.deleteUser();
System.out.println("代理后============================");
userDao1.addUser();
userDao1.deleteUser();
}
}
三、基于代理类的AOP实现
Spring中的AOP代理默认就是使用JDK动态代理的方式来实现的。
1、spring的通知类型
- 环绕通知:在目标方法执行前后实施增强,可以用于日志、事务管理等功能
- 前置通知:在目标方法执行前实施增强,可以用于权限管理等功能
- 后置通知:在目标方法执行后实施增强,可以用于关闭流、上传文件、删除临时文件等功能
- 异常通知:在方法抛出异常后实施增强,可以用于处理异常记录日志等功能
- 最终通知:在后置之后执行
2、ProxyFactoryBean
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean,而ProxyFactoryBean负责为其他Bean创建代理实例。导入spring所需jar包后,还需导入AOP的jar包spring-aop-4.3.6-RELEASE.jar和aopalliance-1.0.jar。在上一个项目中创建factorybean包,同时在该报中创建切面类MyAspect。实现环绕通知。需要实现org.aopalliance.intercept.MethodInterceptor接口:
public class MyAspect implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// TODO Auto-generated method stub
check_Permissions();
//执行目标方法
Object obj = mi.proceed();
log();
return obj;
}
public void check_Permissions() {
System.out.println("模拟检查权限。。。");
}
public void log() {
System.out.println("模拟记录日志。。。");
}
}
创建配置文件applicationContext.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<!-- 目标类 -->
<bean id = "userDao" class = "com.itheima.jdk.UserDaoImpl"/>
<!-- 切面类 -->
<bean id = "myAspect" class = "com.itheima.fctorybean.MyAspect"/>
<!-- 使用Spring代理工厂定义一个名称为userDaoProxy的代理对象 -->
<bean id = "userDaoProxy" class = "org.springframework.aop.framework.ProxyFactoryBean">
<!-- 指定代理接口 -->
<property name = "proxyInterfaces" value = "com.itheima.jdk.UserDao"/>
<!-- 指定目标对象 -->
<property name="target" ref = "userDao"/>
<!-- 指定切面 植入环绕通知 -->
<property name = "interceptorNames" value = "myAspect"/>
<!-- 指定代理方式 true:使用cglib false:(默认) 使用jdk动态代理 -->
<property name ="proxyTargetClass" value = "true"/>
</bean>
</beans>
首先通过<bean>定义了目标类和切面,然后使用ProxyFactoryBean类定义了代理对象,分别通过<property>子元素指定了代理实现的接口,代理的目标对象,需要织入目标类的通知以及代理方式。创建测试类:
public class ProxyFactoryBeanTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
String xmlPath = "com/itheima/fctorybean/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//从Spring容器获得内容
UserDao userDao = (UserDao) applicationContext.getBean("userDaoProxy");
//执行方法
userDao.addUser();
userDao.deleteUser();
}
}
四、AspectJ开发
1、基于XML的声明式AspectJ
指通过XML文件来定义切面、切入点以及通知,所有的切面、切入点和通知必须定义在<aop:config>元素内。spring配置文件中的<beans>可以包含多个<aop:config>元素,一个<aop:config>元素可以包含属性和子元素,其子元素包括<aop:pointcut>、<aop:advisor>和<aop:aspect>配置时,这三个子元素必须按照此顺序来定义。<aop:aspect>元素下,同样包含了属性和多个子元素。常用元素的配置代码如下:
(1)配置切面
配置切面使用的是<aop:aspect>元素,该元素会将一个已经定义好的SpringBean转换成切面Bean,所以应先定义一个普通的SpringBean,通过ref属性可以引用该bean。
(2)配置切入点
切入点通过<aop:pointcut>元素定义,当其作为<aop:config>元素的子元素定义是,表示该切入点为全局切入点,可以被多个切面所共享;当其作为<aop:aspect>的子元素是,表示该切入点只对当前切面有效。其中重要的属性为expression:用于指定切入点管理的切入点表达式。
(3)配置通知
分别使用<aopaspect>的子元素配置常用的5种通知。
- pointcut:用于指定切入点表达式
- pointcut-ref:该属性指定已经存在的切入点名称
- method:指定一个方法名,指定将切面Bean中的方法转换为增强处理
- throwing:指定一个形参名,异常通知方法可以通过该形参访问目标所抛出的异常
- returning:后置方法可以通过该形参访问目标方法的返回值
在上一个项目下创建aspectj.xml包,同时加入jar包:spring-aspects-4.3.6.RELEASE.jar和aspectjweaver-1.8.10.jar。在该包下创建切面类MyAspect:
/**
* 切面类,在此类中编写通知
* @author Youguangfu
*
*/
public class MyAspect {
//前置通知
public void myBefore(JoinPoint joinPoint) {
System.out.println("前置通知:模拟执行权限检查。。。");
System.out.println("目标类是:"+joinPoint.getTarget());
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
//后置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.println("后置通知,模拟记录日志。。。");
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知ProceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始
System.out.println("环绕开始:执行目标方法前,模拟开启事务。。。");
//执行当前目标方法
Object obj = proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法后,模拟关闭事务。。。");
return obj;
}
//异常通知
public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("异常通知:"+"出错了"+e.getMessage());
}
//最终通知
public void myAfter() {
System.out.println("最终通知:模拟方法结束后释放资源");
}
}
分别定义了5中不同类型的通知,环绕通知必须接受一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常。创建配置文件applicationContext.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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"
>
<!-- 1 目标类 -->
<bean id = "user" class = "com.itheima.jdk.UserDaoImpl"/>
<!-- 2 切面 -->
<bean id = "myAspect" class = "com.itheima.aspectj.xml.MyAspect"/>
<!-- 3 aop编程 -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref = "myAspect">
<!-- 3.1配置切入点,通知最后增强哪些方法 -->
<aop:pointcut expression="execution(* com.itheima.jdk.*.*(..))" id="myPointCut"/>
<!-- 3.2 关联通知Advice和切入点pointCut -->
<!-- 3.2.1 前置通知 -->
<aop:before method="myBefore" pointcut-ref = "myPointCut"/>
<!-- 3.2.2 后置通知,在方法返回之后执行,就可以获得返回值 returning属性:用于设置后置通知的第二个参数的名称,类型是Object-->
<aop:after-returning method="myAfterReturning" pointcut-ref = "myPointCut" returning = "returnVal"/>
<!-- 3.2.3环绕通知 -->
<aop:around method="myAround" pointcut-ref = "myPointCut"/>
<!-- 3.2.4 。跑出通知:用于处理程序发生异常 -->
<!-- *注意:如果程序没有异常,将不会执行增强 -->
<!-- *throwing属性:用于设置通知第二个参数的名称,类型Throwable -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref = "myPointCut" throwing = "e"/>
<!-- 3.2.5 最终通知:无论程序发生任何事情,都将执行 -->
<aop:after method="myAfter" pointcut-ref = "myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
***后置通知和最终通知的区别:
虽然都是在目标方法执行后执行,但后置通知只有在目标方法成功执行后才会被织入。而最终通知无论目标方法如何结束(包括成功执行和异常终止两种情况),都会被织入。
创建测试类:
public class TestXmlAspect {
public static void main(String[] args) {
// TODO Auto-generated method stub
String xmlPath = "com/itheima/aspectj/xml/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//从Spring容器中获得内容
UserDao userDao = (UserDao) applicationContext.getBean("user");
userDao.addUser();
}
}
2、基于注解的声明式AspectJ
- @Aspect:定义一个切面
- @Pointcut:定义切入点表达式,使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称。实际这个方法签名就是一个返回为void且方法体为空的普通函数。
- @Before:定义前置通知,相当于BeforeAdvice。使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式。
- @AfterReturning:定义后置通知,使用时可以指定pointcut/value和returning属性
- @Around:定义环绕通知,使用时需要指定一个value属性,该属性用于指定该通知被植入的织入点。
- @AfterThrowing:定义异常通知来处理程序中未处理的异常。
- @After:定义最终通知。
在上一个项目中创建annotion包MyAspect类如下:
/**
* 切面类,在此类中编写通知
* @author Youguangfu
*
*/
@Aspect
@Component
public class MyAspect {
//定义切入点
@Pointcut("execution(* com.itheima.jdk.*.*(..))")
//使用一个返回为void 方法体为空的方法类命名切入点
private void myPoint() {}
//前置通知
@Before(value="myPoint()")
public void myBefore(JoinPoint joinPoint) {
System.out.println("前置通知:模拟执行权限检查。。。");
System.out.println("目标类是:"+joinPoint.getTarget());
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
//后置通知
@AfterReturning("myPoint()")
public void myAfterReturning(JoinPoint joinPoint) {
System.out.println("后置通知,模拟记录日志。。。");
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知ProceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须throws Throwable
*/
@Around("myPoint()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始
System.out.println("环绕开始:执行目标方法前,模拟开启事务。。。");
//执行当前目标方法
Object obj = proceedingJoinPoint.proceed();
//结束
System.out.println("环绕结束:执行目标方法后,模拟关闭事务。。。");
return obj;
}
//异常通知
@AfterThrowing(value="myPoint()" ,throwing="e")
public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("异常通知:"+"出错了"+e.getMessage());
}
//最终通知
@After("myPoint()")
public void myAfter() {
System.out.println("最终通知:模拟方法结束后释放资源");
}
}
使用@Aspect定义了切面类,使用@Pointcut哦诶之qirud切入点。配置文件applicationContext.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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>
<!-- 指定需要扫描的包,使注释生效 -->
<context:component-scan base-package="com.itheima" />
<!-- 启动基于注解的声明式AspectJ支持 -->
<aop:aspectj-autoproxy/>
</beans>
测试:
public class TestXmlAspect {
public static void main(String[] args) {
// TODO Auto-generated method stub
String xmlPath = "com/itheima/aspectj/annotation/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//从Spring容器中获得内容
UserDao userDao = (UserDao) applicationContext.getBean("user");
userDao.addUser();
}
}