一、简介
AOP全程是Aspect—Oriented—Programming,面向切面编程。AOP采用横向抽取机制,将分散在各个方法中的重复代码抽取出来,然后在程序编译或运行时,再将这些提取出来的代码应用到需要执行的地方。
使用AOP编程,可以使开发人员专心于核心业务,而不用过多的关注于其他业务逻辑的是实现,不但提高了开发效率,而且增强了代码的可维护性。
目前最流行的AOP框架有两个,分别是Spring AOP和AspectJ。其中:Spring AOP采用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类织入增强的代码。AspectJ是一个基于Java语言的AOP框架,从Spring 2.0开始,AspectJ扩展了Java语言,扩展了一个专门的编译器,在编译时提供横向代码的织入。
二、重点解析
1、代理分类
- Pointcut:全局切入点
- Before:前置通知
- After-returning:后置通知
- Around:环绕通知
- After-throwing:异常通知
- After:最终通知
- Declare-parents:引介通知
2、切入点位置配置(expression)
// 定义切入点表达式 @Pointcut("execution(* com.lydms.jdk.*.*(..))") private void myPointCut(){}
- 第1个*:表示返回类型,*****表示任意返回类型。
- com.lydms.jdk:表示需要拦截的包名。
- 第2个*:表示类名,*****表示所有类。
- 第3个*:表示方法名,*****表示所有方法。
- (…):表示方法的参数,”…“表示任意参数。
- 第一个*与包名之间有空格。
3、配置通知
属性名称 | 描述 |
pointcut | 该属性用于指定一个切入点表达式,Spring将在匹配表达式的连接点时织入该通知。 |
pointcut-ref | 该属性指定一个已经存在的切入点名称。通常pointcut和pointcut-ref两个属性只需要其中之一。 |
method | 该属性指定一个方法名。指定将切面Bean中的该方法转换为增强处理。 |
throwing | 该属性只对<after-throwing> 元素有效,它用于指定一个形参名,异常通知方法可以通过该形参访问目标方法所抛出的异常。 |
returning | 该属性只对<after-returning> 元素有效。它用于指定一个形参名,后置通知方法可以通过该形参访问目标方法的返回值。 |
案例一:表示任意方法的任意类任意参数都走切面。
@Pointcut("execution(* com.lydms.jdk.*.*(..))") private void myPointCut(){ }
案例二:public修饰符,在upload类下的。方法走切面。
@Pointcut("execution(public * com.lydms.controller.upload.*(..))") public void webLog() { }
二、动态代理
- Sring AOP中的代理。可以是JDK动态代理,也可以是CGLIB代理。
- JDK动态代理实现很简单,但是有一定局限性—使用动态代理的对象必须实现一个或多个接口。假如没有实现的,则使用CGLIB代理。
1、JDK动态代理
JDK动态代理通过java.lang.reflect.proxy来实现。可以调用Proxy类的newProxyInstance( )方法来创建代理对象。对于使用业务接口的类,Spring 默认使用JDK动态代理来实现AOP。
1)、UserDao接口
public interface UserDao { public void addUser(); public void deleteUser(); }
2)、UserDaoImpl实现类
import org.springframework.stereotype.Repository; // 目标类 @Repository("userDao") public class UserDaoImpl implements UserDao { public void addUser() { // int i = 10/0; System.out.println("添加用户"); } public void deleteUser() { System.out.println("删除用户"); } }
3)、代理实现类
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import com.lydms.aspect.MyAspect; /** * JDK代理类 */ public class JdkProxy implements InvocationHandler{ // 声明目标类接口 private UserDao userDao; // 创建代理方法 public Object createProxy(UserDao userDao) { this.userDao = userDao; // 1.类加载器 ClassLoader classLoader = JdkProxy.class.getClassLoader(); // 2.被代理对象实现的所有接口 Class[] clazz = userDao.getClass().getInterfaces(); // 3.使用代理类,进行增强,返回的是代理后的对象 return Proxy.newProxyInstance(classLoader,clazz,this); } /* * 所有动态代理类的方法调用,都会交由invoke()方法去处理 * proxy 被代理后的对象 * method 将要被执行的方法信息(反射) * args 执行方法时需要的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 声明切面 MyAspect myAspect = new MyAspect(); // 前增强 myAspect.check_Permissions(); // 在目标类上调用方法,并传入参数 Object obj = method.invoke(userDao, args); // 后增强 myAspect.log(); return obj; } }
4)、测试类
public class JdkTest{ public static void main(String[] args) { // 创建代理对象 JdkProxy jdkProxy = new JdkProxy(); // 创建目标对象 UserDao userDao= new UserDaoImpl(); // 从代理对象中获取增强后的目标对象 UserDao userDao1 = (UserDao) jdkProxy.createProxy(userDao); // 执行方法 userDao1.addUser(); userDao1.deleteUser(); } }
执行结果:
2、CGLIB代理
是一个高性能的开源的代码生成包,采用非常底层字节码技术,对指定的目标类生成子类,并对子类进行加强。
1)、UserDao.java
//目标类 public class UserDao { public void addUser() { System.out.println("添加用户"); } public void deleteUser() { System.out.println("删除用户"); } }
2)、代理类
import java.lang.reflect.Method; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import com.lydms.aspect.MyAspect; // 代理类 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 CGlib根据指定父类生成的代理对象 * method 拦截的方法 * args 拦截方法的参数数组 * methodProxy 方法的代理对象,用于执行父类的方法 */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 创建切面类对象 MyAspect myAspect = new MyAspect(); // 前增强 myAspect.check_Permissions(); // 目标方法执行 Object obj = methodProxy.invokeSuper(proxy, args); // 后增强 myAspect.log(); return obj; } }
3)、测试类
// 测试类 public class CglibTest { public static void main(String[] args) { // 创建代理对象 CglibProxy cglibProxy = new CglibProxy(); // 创建目标对象 UserDao userDao = new UserDao(); // 获取增强后的目标对象 UserDao userDao1 = (UserDao)cglibProxy.createProxy(userDao); // 执行方法 userDao1.addUser(); userDao1.deleteUser(); } }
执行结果:
四、AspectJ开发
1、基于XML的声明式AspectJ
1) 简介
2) 切面类
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; /** *切面类,在此类中编写通知 */ public class MyAspect { // 前置通知 public void myBefore(JoinPoint joinPoint) { System.out.print("前置通知 :模拟执行权限检查...,"); System.out.print("目标类是:"+joinPoint.getTarget() ); System.out.println(",被织入增强处理的目标方法为:" +joinPoint.getSignature().getName()); } // 后置通知 public void myAfterReturning(JoinPoint joinPoint) { System.out.print("后置通知:模拟记录日志...," ); 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("最终通知:模拟方法结束后的释放资源..."); } }
3) 注解
<?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="userDao" class="com.lydms.jdk.UserDaoImpl" /> <!-- 2 切面 --> <bean id="myAspect" class="com.lydms.aspectj.xml.MyAspect" /> <!-- 3 aop编程 --> <aop:config> <!-- 配置切面 --> <aop:aspect ref="myAspect"> <!-- 3.1 配置切入点,通知最后增强哪些方法 --> <aop:pointcut expression="execution(* com.lydms.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>
4) 测试类
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.lydms.jdk.UserDao; // 测试类 public class TestXmlAspectj { public static void main(String args[]) { String xmlPath = "com/lydms/aspectj/xml/applicationContext.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); // 1 从spring容器获得内容 UserDao userDao = (UserDao) applicationContext.getBean("userDao"); // 2 执行方法 userDao.addUser(); } }
2、基于注解的声明式AspectJ
1)注解及其描述:
注解名称 | 描述 |
@Aspect | 用于定义一个切面。 |
@Pointcut | 用于定义切入点表达式。在使用时,还需要定义方法签名(包含名字和任意参数),来表示切入点名称。 |
@Before | 用于定义前置通知。相当于BeforeAdvice。通常指定一个value值,用来指定切入点表达式。 |
@AfterReturning | 用于定义后置通知。相当于AfterReturningAdvice。 |
@Around | 用于定义环绕通知。相当于MethodInterceptor。 |
@AfterThrowing | 用于定义异常通知来处理程序中未处理的异常。相当于ThrowAdvice。 |
@After | 用于定义最终final通知。无论是否异常,都会执行。 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor。 |
2)切面类:
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * 切面类,在此类中编写通知 */ @Aspect @Component public class MyAspect { // 定义切入点表达式 @Pointcut("execution(* com.lydms.jdk.*.*(..))") // 使用一个返回值为void、方法体为空的方法来命名切入点 private void myPointCut(){} // 前置通知 @Before("myPointCut()") public void myBefore(JoinPoint joinPoint) { System.out.print("前置通知 :模拟执行权限检查...,"); System.out.print("目标类是:"+joinPoint.getTarget() ); System.out.println(",被织入增强处理的目标方法为:" +joinPoint.getSignature().getName()); } // 后置通知 @AfterReturning(value="myPointCut()") public void myAfterReturning(JoinPoint joinPoint) { System.out.print("后置通知:模拟记录日志...," ); System.out.println("被织入增强处理的目标方法为:" + joinPoint.getSignature().getName()); } // 环绕通知 @Around("myPointCut()") public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 开始 System.out.println("环绕开始:执行目标方法之前,模拟开启事务..."); // 执行当前目标方法 Object obj = proceedingJoinPoint.proceed(); // 结束 System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..."); return obj; } // 异常通知 @AfterThrowing(value="myPointCut()",throwing="e") public void myAfterThrowing(JoinPoint joinPoint, Throwable e) { System.out.println("异常通知:" + "出错了" + e.getMessage()); } // 最终通知 @After("myPointCut()") public void myAfter() { System.out.println("最终通知:模拟方法结束后的释放资源..."); } }
3)注解配置要扫描的包:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/context/spring-context-4.3.xsd"> <!-- 指定需要扫描的包,使注解生效 --> <context:component-scan base-package="com.lydms" /> <!-- 启动基于注解的声明式AspectJ支持 --> <aop:aspectj-autoproxy /> </beans>
4)测试类:
import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.lydms.jdk.UserDao; // 测试类 public class TestAnnotationAspectj { public static void main(String args[]) { String xmlPath = "com/lydms/aspectj/annotation/applicationContext.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); // 1 从spring容器获得内容 UserDao userDao = (UserDao) applicationContext.getBean("userDao"); // 2 执行方法 userDao.addUser(); } }
五、Spring boot集成AspectJ开发
1、Pom.xml
<!--不指定版本的原因,如不指定版本,会默认下载springboot对应版本的jar --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2、Controller.java
import com.lydms.common.Result; import com.lydms.pojo.English; import com.lydms.service.WordAddService; import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @RestController @RequestMapping("/add") public class WordAddController { private static final Logger logger = LogManager.getLogger(WordAddController.class); @Autowired private WordAddService wordAddService; /** * 添加英语 * @throws Exception */ @RequestMapping(value = "/english", method = {RequestMethod.POST}) public ApiResult addEnglish(@RequestBody English en) { // 1、将单词存入数据库 English result = wordAddService.addEnglish(en); return ApiResult.succ(result); } }
3、切面类(Aspect.java)
import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @Aspect @Order(5) @Component public class ControllerAspect { private static final Logger logger = LogManager.getLogger(ControllerAspect.class); private ThreadLocal<Long> startTime = new ThreadLocal<Long>(); @Autowired private ObjectMapper mapper; public ControllerAspect() { } @Pointcut("execution(public * com.ldyms.controller.*.*(..))") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { startTime.set(System.currentTimeMillis()); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Enumeration<String> enums = request.getParameterNames(); ArrayList params = new ArrayList(); while (enums.hasMoreElements()) { String paraName = enums.nextElement(); String param = paraName + ":" + request.getParameter(paraName); params.add(param); } logger.info("request Url:{},Params:{}",(request.getRequestURL().toString()),params); logger.info("request Methods:{}", Arrays.toString(joinPoint.getArgs())); } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) throws Throwable { logger.info("RESPONSE: {} ", this.mapper.writeValueAsString(ret)); logger.info("SPEND TIME: {} ms", System.currentTimeMillis() - startTime.get()); } }