一、Spring AOP简单介绍
1、AOP简单介绍
- AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
- AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
- 使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
- Spring中的AOP代理还是离不开Spring的IOC容器,代理的生成,管理及其依赖关系都是由IOC容器负责,Spring默认使用JDK动态代理,在需要代理类而不是代理接口的时候,Spring会自动切换为使用CGLIB代理,不过现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些。
2、AOP的概念
横切关注点:
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点(概念)- 切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
- 连接点(joinpoint):被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
- 切入点(pointcut):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
- 通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置(before)、后置(afterReturning)、异常(afterThrowing)、最终(after)、环绕通知(around)五类
- 目标对象:代理的目标对象
- 织入(weave):将切面应用到目标对象并导致代理对象创建的过程
- 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
3、AOP开发步骤
- 定义普通业务组件
- 定义切入点,一个切入点可能横切多个业务组件
- 定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作
所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。
二、Spring集成AOP开发
- pom文件添加依赖
<!--aop--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> - 切面类、切入点、通知、连接点代码
package com.wxx.demo.aop; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Maps; import com.wxx.demo.model.HelloModel; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @Component @Aspect//切面类 public class HelloAspect { private static final Logger logger = LoggerFactory.getLogger(HelloAspect.class); //凡是注解了RequestMapping的方法都被拦截 @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") private void webPointcut() { } //指定切入点 @Pointcut("execution(* com.wxx.demo.controller.HelloController.*(..)))") private void validate() { } //通知advice @Before("validate()") public void doBefore(JoinPoint joinPoint) {//通过joinpoint获取通知的签名信息如目标名,参数信息 System.out.println("========================前置通知========================"); Object[] args = joinPoint.getArgs(); joinPoint.getThis();//aop代理信息 System.out.println("========================aop代理信息:" + joinPoint.getThis() + "========================"); joinPoint.getTarget();//代理对象 System.out.println("========================aop代理对象:" + joinPoint.getTarget() + "========================"); Signature signature = joinPoint.getSignature(); System.out.println("========================aop通知签名:" + signature + "========================"); String methodName = signature.getName();//代理方法名 System.out.println("========================aop代理方法名:" + methodName + "========================"); // AOP 代理的名字 System.out.println("========================aop代理的名字:" + signature.getDeclaringTypeName() + "========================"); signature.getDeclaringType();// AOP代理类的类(class)信息 /** * 通过RequestContextHolder获取请求信息,如session 信息 ; * 注: 关于调用 JoinPoint 和 RequestContextHolder。 通过JoinPoint可以获得通知的签名信息,如目标方法名、目标方法参数信息等。 通过RequestContextHolder来获取请求信息,Session信息。 */ // 获取RequestAttributes RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 从requestAttributes中获取HttpServletRequest信息 HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST); // 获取session信息 HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION); System.out.println("请求 : " + request + " , HttpSession : " + session); Enumeration<String> enumerations = request.getParameterNames(); // Map<String,String> parameterMaps=new HashMap<>(); Map<String, String> parameterMaps = Maps.newHashMap(); while (enumerations.hasMoreElements()) { String parameter = enumerations.nextElement(); parameterMaps.put(parameter, request.getParameter(parameter)); } // String str=JSON.toJSONString(parameterMaps); String str = JSON.toJSONString(parameterMaps);// alibaba.fastjson if (args.length > 0) { System.out.println("请求参数信息为 : " + str); } } /** * 后置返回通知 * 需要注意: * 如果第一个参数是JoinPoint,则第二个参数是返回值的信息 * 如果参数中的第一个不是JoinPoint,则第一个参数是returning中对应的参数, * returning 限定了只有目标方法返回值与通知方法相应参数类型时才能 * 执行后置返回通知,否则不执行; * 对于returning对应的通知方法参数为Object类型将匹配任何目标返回值 * @param joinPoint * @param keys * value = "execution(* com.wxx.demo.controller..*.*(..))" */ @AfterReturning(pointcut = "validate()",returning = "keys") public void doAfterReturn(JoinPoint joinPoint,Object keys){ System.out.println("========================后置返回通知执行========================"); if (keys instanceof HelloModel){ HelloModel hello = (HelloModel) keys; hello.setHello("hello aop i am @AfterReturning!"); System.out.println("========================后置返回通知修改后的参数:" + keys.toString() + "========================"); } } /** * 后置异常通知 * @param e */ @AfterThrowing(pointcut = "validate()",throwing = "e") public void doAfterThrowing(Exception e){ //if (e instanceof FieldError) Map<String,Object> map = new HashMap<>(); map.put("resCode",500); map.put("resMsg","Illegal parameters"); writeContent(JSONObject.toJSONString(map)); } /** * 后置最终通知 * */ @After(value = "validate()") public void doAfter(){ System.out.println("========================后置通知最终执行了========================"); } /** * 拦截web层异常, * 记录异常日志,并返回友好信息到前端 * 目前只拦截Exception,是否要拦截Error需再做考虑 * * @param e 异常对象 */ @AfterThrowing(pointcut = "webPointcut()", throwing = "e") public void handleThrowing(Exception e) { e.printStackTrace(); logger.error("发现异常!" + e.getMessage()); logger.error(JSON.toJSONString(e.getStackTrace())); //这里输入友好性信息 Map<String, Object> map = new HashMap<>(); map.put("resCode", "500"); map.put("resMsg", "laoma is dead"); writeContent(JSONObject.toJSONString(map)); } /** * 将内容输入浏览器 * * @param content */ private void writeContent(String content) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); response.reset(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type", "text/plain;charset=UTF-8"); response.setHeader("icop-content-type", "exception"); PrintWriter writer = null; try { writer = response.getWriter(); } catch (IOException e) { e.printStackTrace(); } writer.print(content); writer.flush(); writer.close(); } } - Controller方法代码
/** * 参数校验 * @param helloModel * @param bindingResult */ @GetMapping("/validate") public HelloModel validate(@Valid HelloModel helloModel, BindingResult bindingResult) { HelloValidate.validate(bindingResult); return helloModel; } - 代码运行实例,浏览器访问http://localhost:8086/api/validate?hello= &phone="123456789"执行正常的日志信息及页面返回

========================前置通知======================== ========================aop代理信息:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop代理对象:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop通知签名:HelloModel com.wxx.demo.controller.HelloController.validate(HelloModel,BindingResult)======================== ========================aop代理方法名:validate======================== ========================aop代理的名字:com.wxx.demo.controller.HelloController======================== 请求 : org.apache.catalina.connector.RequestFacade@5288ea44 , HttpSession : org.apache.catalina.session.StandardSessionFacade@89f7377 请求参数信息为 : {"phone":"“123456789”","hello":" "} ========================后置通知最终执行了======================== ========================后置返回通知执行======================== ========================后置返回通知修改后的参数:HelloModel{hello='hello aop i am @AfterReturning!', phone='“123456789”', email='null'}======================== - 执行异常的日志及页面返回
========================前置通知======================== ========================aop代理信息:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop代理对象:com.wxx.demo.controller.HelloController@6cdae95d======================== ========================aop通知签名:HelloModel com.wxx.demo.controller.HelloController.validate(HelloModel,BindingResult)======================== ========================aop代理方法名:validate======================== ========================aop代理的名字:com.wxx.demo.controller.HelloController======================== 请求 : org.apache.catalina.connector.RequestFacade@5288ea44 , HttpSession : org.apache.catalina.session.StandardSessionFacade@89f7377 请求参数信息为 : {"phone":"","hello":" "} ========================后置通知最终执行了======================== 2018-12-28 17:53:19.570 ERROR 10632 --- [nio-8086-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/api] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: 手机号不能为空!] with root cause java.lang.IllegalArgumentException: 手机号不能为空! at org.springframework.util.Assert.isTrue(Assert.java:92) ~[spring-core-4.3.21.RELEASE.jar:4.3.21.RELEASE]

采坑总结:该实例为springmvc的参数校验和异常处理用aop统一处理,学习中遇到的坑
启动报错
Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut
原因是配置不同通知的时候参数是否配置比如:

