网站推广由什么样的人来做,做海报的网站什么编辑,做ppt常用网站,seo关键词外包Spring AOP面向切面编程 面向切面编程AOP作用AOP功能AOP总结 AOP核心概念AOP的实现方式Spring 对AOP支持支持Aspect声明一个切面声明一个切入点AspectJ描述符如下AspectJ类型匹配的通配符常用的匹配规则 声明增强 用AOP实现日志拦截一般的实现仅拦截需要的方法先定义一个日志注… Spring AOP面向切面编程 面向切面编程AOP作用AOP功能AOP总结 AOP核心概念AOP的实现方式Spring 对AOP支持支持Aspect声明一个切面声明一个切入点AspectJ描述符如下AspectJ类型匹配的通配符常用的匹配规则 声明增强 用AOP实现日志拦截一般的实现仅拦截需要的方法先定义一个日志注解Log用annotation定义切入点在想做日志输出的方法上使用注解Log requestId传递 关于增强执行顺序 面向切面编程 面向切面编程AOPAspect Oriented Programming是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。 AOP作用
利用AOP可以对业务逻辑的各个部分进行隔离从而使得业务逻辑各部分之间的耦合度降低提高程序的可重用性同时提高了开发的效率。
AOP功能
主要功能日志记录、性能统计、安全控制、事务处理、异常处理等。
AOP总结
面向切面编程是希望能够将通用需求功能从不相关的类当中分离出来能够使得很多类共享一个行为一旦发生变化不必修改很多类而只是修改这个行为即可。
AOP通过提供另一种思考程序结构的方式来补充了面向对象编程OOP。OOP中模块化的基本单元是类(class)而AOP中模块化的基本单元是切面(aspect)。可以这么理解OOP是解决了纵向的代码复用问题AOP是解决了横向的代码复用问题。
Spring的关键组件之一是AOP框架。虽然Spring IOC容器不依赖于AOP意味着如果你不想使用AOP则可以不使用AOP但AOP补充了Spring IOC以提供一个非常强大的中间件解决方案。 AOP核心概念
切面aspect 在AOP中切面一般使用Aspect注解来标识。连接点Join Point 在Spring AOP一个连接点总是代表一次方法的执行。增强Advice 在连接点执行的动作。切入点Pointcout 说明如何匹配到连接点。引介Introduction 为现有类型声明额外的方法和属性。目标对象Target Object 由一个或者多个切面建议的对象也被称为建议对象由于Spring AOP是通过动态代理来实现的这个对象永远是一个代理对象。AOP代理AOP proxy 一个被AOP框架创建的对象用于实现切面约定增强方法的执行等。在Spring Framework中一个AOP代理是一个JDK动态代理或者CGLIB代理。织入Weaving 连接切面和目标对象或类型创建代理对象的过程。它能在编译时例如使用AspectJ编译器、加载时或者运行时完成。Spring AOP与其他的纯Java AOP框架一样是在运行时进行织入的。 Spring AOP包括以下类型的增强
前置增强(Before advice)在连接点之前运行但不能阻止到连接点的流程继续执行除非抛出异常返回增强(After returning advice)在连接点正常完成后运行的增强例如方法返回没有抛出异常异常增强(After thorwing advice)如果方法抛出异常退出需要执行的增强后置增强(After (finally) Advice)无论连接点是正常或者异常退出都会执行该增强环绕增强(Around advice)围绕连接点的增强例如方法的调用。环绕增强能在方法的调用之前和调用之后自定义行为。它还可以选择方法是继续执行或者去缩短方法的执行通过返回自己的值或者抛出异常。
AOP的实现方式
AOP的两种实现方式静态织入以AspectJ为代表和动态代理Spring AOP实现。
AspectJ是一个采用Java实现的AOP框架它能够对代码进行编译在编译期进行让代码具有AspectJ的AOP功能当然它也可支持动态代理的方式Spring AOP实现通过动态代理技术来实现Spring2.0集成了AspectJ主要用于PointCut的解析和匹配底层的技术还是使用的Spring1.x中的动态代理来实现。 Spring AOP实现采用了两种混合的实现方式JDK动态代理和CGLib动态代理。
JDK动态代理Spring AOP的首选方法。每当目标对象实现一个接口时就会使用JDK动态代理。目标对象必须实现接口。CGLIB如果目标对象没有实现接口则可以使用CGLIB代理。 Spring 对AOP支持
Spring可以使用两种方式来实现AOP基于注解式配置和基于XML配置。
下面介绍基于注解配置的形式
支持Aspect
如果是Spring Framework需要使用aspectjweaver.jar包然后创建我们自己的AppConfig如下并加上EnableAspectJAutoProxy注解开启AOP代理自动配置(Spring Boot默认是开启的则不需要增加配置)如下
Configuration
EnableAspectJAutoProxy
public class AppConfig {}声明一个切面
Aspect //告诉Spring 这是一个切面
Component //交给Spring容器管理
public class MyAspect {}可以使用Aspect来定义一个类作为切面但是这样该类并不会自动被Spring加载还是需要加上Component注解。
声明一个切入点
一个切入点的生命包含两个部分一个包含名称和任何参数的签名和一个切入点的表达式这个表达式确定了我们对哪些方法的执行感兴趣。
我们以拦截Controller层中的MyController中的test方法为例子代码如下
RestController
RequestMapping(/my)
public class MyController {GetMapping(/test)public void test() {System.out.println(test 方法);}
}下面定义一个名为controller的切入点该切入点与上述的test方法相匹配切入点需要用Pointcut注解来标注如下
//表达式
Pointcut(execution (public * com.yc.springboot.controller.MyController.test()))
public void controller(){}; //签名切入点表达式的格式如下
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(方法修饰符(可选) 返回类型 类路径 方法名 参数 异常模式可选)AspectJ描述符如下
AspectJ描述符描述arg()限制连接点匹配参数为指定类型的执行方法args()限制连接点匹配参数由指定注解标注的执行方法execution()用于匹配是连接点的执行方法this()限制连接点匹配的AOP代理的bean引用为指定类型的类target限制连接点匹配目标对象为指定类型的类target()限制连接点匹配特定的执行对象这些对象对应的类要具有指定类型的注解within()限制连接点匹配指定的类型within()限制连接点匹配指定注解所标注的类型annotationn限定匹配带有指定注解的连接点
常用的主要是execution()
AspectJ类型匹配的通配符
通配符含义*匹配任何数量字符..匹配任何数量字符的重复 匹配指定类型的子类型仅能用于后缀放在类型模式后面
常用的匹配规则
表达式内容execution(public * *(..))匹配所有public方法execution(* set*(..))匹配所有方法名开头为set的方法execution(* com.xyz.service.AccountService.*(..))匹配AccountService下的所有方法execution(* com.xyz.service.*.*(..))匹配service包下的所有方法execution(* com.xyz.service..*.*(..))匹配service包或其子包下的所有方法annotation(org.springframework.transaction.annotation.Transactional)匹配所有打了Transactional注解的方法bean(*Service)匹配命名后缀为Service的类的方法
声明增强
增强与切点表达式相关联并且在与切点匹配的方法之前、之后或者前后执行。
下面直接罗列了各种增强的声明用于拦截MyController中的各个方法
Aspect //告诉Spring 这是一个切面
Component //告诉Spring容器需要管理该对象
public class MyAspect {//通过规则确定哪些方法是需要增强的Pointcut(execution (public * com.yc.springboot.controller.MyController.*(..)))public void controller() {}//前置增强Before(controller())public void before(JoinPoint joinPoint) {System.out.println(before advice);}//返回增强AfterReturning(pointcut controller(),returning retVal)public void afterReturning(Object retVal) {System.out.println(after returning advice, 返回结果 retVal: retVal);}//异常增强AfterThrowing(pointcut controller(),throwing ex)public void afterThrowing(Exception ex) {System.out.println(after throwing advice, 异常 ex: ex.getMessage());}//后置增强After(controller())public void after(JoinPoint joinPoint) {System.out.println(after advice);}//环绕增强Around(controller())public Object around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println(before advice);//相当于是before adviceObject reVal null;try {reVal joinPoint.proceed();} catch (Exception e) {//相当于afterthrowing adviceSystem.out.println(afterthrowing advice);}//相当于是after adviceSystem.out.println(after advice);return reVal;}
}需要注意的是
在返回增强中我们需要给AfterReturing设置returning的值且需要与方法的参数名一致用于表示业务方法的返回值在异常增强中需要给AfterThrowing设置throwing的值且需要与方法的参数名一致用于表示业务方法产生的异常在环绕增强中参数为ProceedingJoinPoint类型它是JoinPoint的子接口我们需要在这个方法中手动调用其proceed方法来触发业务方法在所有的增强方法中都可以申明第一个参数为JoinPoint(注意的是环绕增强是使用ProceedingJoinPoint来进行申明它实现了JoinPoint接口)JoinPoint接口提供了几个有用的方法 getArgs()返回这个方法的参数getThis()返回这个代理对象getTarget()返回目标对象(被代理的对象)getSignature()返回被增强方法的描述toString()打印被增强方法的有用描述
下面为Mycontroller测试类
RestController
RequestMapping(/my)
public class MyController {GetMapping(/testBefore)public void testBefore() {System.out.println(testBefore 业务方法);}GetMapping(/testAfterReturning)public String testAfterReturning() {System.out.println(testAfterReturning 业务方法);return 我是一个返回值;}GetMapping(/testAfterThrowing)public void testAfterThrowing() {System.out.println(testAfterThrowing 业务方法);int a 0;int b 1 / a;}GetMapping(/testAfter)public void testAfter() {System.out.println(testAfter 业务方法);}GetMapping(/around)public void around() {System.out.println(around 业务方法);}
}用AOP实现日志拦截
一般的实现
打印日志是AOP的一个常见应用场景我们可以对Controller层向外提供的接口做统一的日志拦截用日志记录请求参数、返回参数、请求时长以及异常信息方便我们线上排查问题下面是核心类LogAspect的实现
/*** 日志的切面*/
Aspect
Component
public class LogAspect {Resourceprivate IdWorker idWorker;Pointcut(execution (public * com.yc.core.controller.*.*(..)))public void log(){}/*** 使用环绕增强实现日志打印* param joinPoint* return* throws Throwable*/Around(log())public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {//获得执行方法的类和名称String className joinPoint.getTarget().getClass().getName();//获得方法名MethodSignature signature (MethodSignature) joinPoint.getSignature();String methodName signature.getMethod().getName();//获得参数Object[] args joinPoint.getArgs();long requestId idWorker.nextId();//打印参数LogHelper.writeInfoLog(className, methodName, requestId: requestId ,params: JSONObject.toJSONString(args));long startTime System.currentTimeMillis();//执行业务方法Object result null;try {result joinPoint.proceed();} catch (Exception e) {LogHelper.writeErrLog(className, methodName, requestId: requestId ,异常啦 LogAspect.getStackTrace(e));}long endTime System.currentTimeMillis();//打印结果LogHelper.writeInfoLog(className, methodName, requestId: requestId ,耗时 (endTime - startTime) msresult: JSONObject.toJSONString(result));//返回return result;}/*** 获取异常的堆栈信息* param throwable* return*/public static String getStackTrace(Throwable throwable){StringWriter sw new StringWriter();PrintWriter pw new PrintWriter(sw);try{throwable.printStackTrace(pw);return sw.toString();} finally{pw.close();}}
}在proceed()方法之前相当于前置增强收集类名、方法名、参数记录开始时间生成requestId在proceed()方法之后相当于后置增强并能获取到返回值计算耗时在前置增强时生成requestId用于串联多条日志使用try、catch包裹proceed()方法在catch中记录异常日志提供了getStackTrace方法获取异常的堆栈信息便于排查报错详细情况
仅拦截需要的方法
但是上面的日志是针对所有controller层中的方法进行了日志拦截如果我们有些方法不想进行日志输出比如文件上传的接口、大量数据返回的接口这个时候定义切入点的时候可以使用annotation描述符来匹配加了特定注解的方法步骤如下
先定义一个日志注解Log
/*** 自定义日志注解*/
Target(ElementType.METHOD)
Retention(RetentionPolicy.RUNTIME)
public interface Log {
}用annotation定义切入点
Pointcut(annotation(com.yc.core.annotation.Log))
public void logAnnotation(){}在想做日志输出的方法上使用注解Log
Log
PostMapping(value testannotation)
public AOPTestVO testannotation(RequestBody AOPTestDTO aopTestDTO) {AOPTestVO aopTestVO new AOPTestVO();aopTestVO.setCode(1);aopTestVO.setMsg(哈哈哈);return aopTestVO;
}这样我们就可以自定义哪些方法需要日志输出了。
requestId传递
后来有同事提到如果这是针对Controller层的拦截但是Service层也有自定义的日志输出怎么在Service层获取到上述的requestId呢
其实就是我们拦截之后是否可以针对方法的参数进行修改呢其实注意是看
result joinPoint.proceed();我们发现ProceedingJoinPoint还有另外一个带有参数的proceed方法定义如下
public Object proceed(Object[] args) throws Throwable;我们可以利用这个方法在环绕增强中去增加requestId这样后面的增强方法或业务方法中就能获取到这个requestId了。
首先我们先定义一个基类AOPBaseDTO只有一个属性requestId
Data
ApiModel(aop参数基类)
public class AOPBaseDTO {ApiModelProperty(value 请求id, hidden true)private long requestId;
}然后我们让Controller层接口的参数AOPTestDTO继承上述AOPBaseDTO如下
Data
ApiModel(aop测试类)
public class AOPTestDTO extends AOPBaseDTO{ApiModelProperty(value 姓名)private String name;ApiModelProperty(value 年龄)private int age;
}最后在环绕的增强中添加上requestId如下
/*** 使用环绕增强实现日志打印* param joinPoint* return* throws Throwable*/
Around(logAnnotation())
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {//获得执行方法的类和名称String className joinPoint.getTarget().getClass().getName();//获得方法名MethodSignature signature (MethodSignature) joinPoint.getSignature();String methodName signature.getMethod().getName();//获得参数Object[] args joinPoint.getArgs();long requestId idWorker.nextId();for(int i 0; i args.length; i) {if (args[i] instanceof AOPBaseDTO) {//增加requestId((AOPBaseDTO) args[i]).setRequestId(requestId);}}//打印参数LogHelper.writeInfoLog(className, methodName, requestId: requestId ,params: JSONObject.toJSONString(args));long startTime System.currentTimeMillis();//执行业务方法Object result null;try {result joinPoint.proceed(args);} catch (Exception e) {LogHelper.writeErrLog(className, methodName, requestId: requestId ,异常啦 LogAspect.getStackTrace(e));}long endTime System.currentTimeMillis();//打印结果LogHelper.writeInfoLog(className, methodName, requestId: requestId ,耗时 (endTime - startTime) msresult: JSONObject.toJSONString(result));//返回return result;
}我们运行起代码访问一下下面是运行结果 可以看到我们的业务方法中已经能获取到requestId如果Service层需要可以通过传递AOPTestDTO从中获取。
关于增强执行顺序
针对不同类型的增强顺序固定的比如before在after前面针对同一切面的相同类型的增强根据定义先后顺序依次执行针对不同切面的相同增强可以通过使我们的切面实现Ordered接口重写getOrder方法返回值最小优先级越高。注意的是before和after的相反的before的优先级越高越早执行after的优先级越高越晚执行。