Spring AOP 是 Java 面试的必考点,我们需要了解 AOP 的基本概念及原理。那么 Spring AOP 到底是啥,为什么面试官这么喜欢问它呢?本文先介绍 AOP 的基本概念,然后根据 AOP 原理,实现一个接口返回统一格式的小示例,方便大家理解 Spring AOP 到底如何用!
<!--more-->
在实际的开发过程中,我们的应用程序会被分为很多层。通常来讲一个 Java 的 Web 程序会拥有以下几个层次:
虽然看起来每一层都做着全然不同的事情,但是实际上总会有一些类似的代码,比如日志打印和异常处理等。如果我们选择在每一层都独立编写这部分代码,那么久而久之代码将变的很难维护。所以我们提供了另外的一种解决方案:AOP。这样可以保证这些通用的代码被聚合在一起维护,而且我们可以灵活的选择何处需要使用这些代码。
AOP(Aspect Oriented Programming,面向切面编程),可以说是 OOP(Object Oriented Programing,面向对象编程)的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用来模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如权限管理、异常处理等也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而 AOP 技术则恰恰相反,它利用一种称为 “横切” 的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为 “Aspect” ,即切面。 所谓“切面”,简单地说,就是将权限、事务、日志、异常等与业务逻辑相对独立的功能抽取封装,便于减少系统的重复代码,降低模块间的耦合度,增加代码的可维护性。 AOP 代表的是一个横向的关系,如果说 “对象” 是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向切面编程,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息,然后又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
切面理解:用刀将西瓜分成两瓣,切开的切口就是切面;炒菜、锅与炉子共同来完成炒菜,锅与炉子就是切面。Web 层级设计中,Controller 层、Service 层、Dao 层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
推荐网上的一篇通俗易懂的 AOP 理解: https://blog.csdn.net/qukaiwe... 。
AOP有很多专业术语,初看这么多术语,可能一下子不大理解,多读几遍,相信很快就会搞懂。
就是 Spring 允许你放通知(Advice)的地方,很多,基本每个方法的前、后(两者都有也行)或抛出异常时都可以是连接点,Spring 只支持方法连接点,和方法有关的前前后后都是连接点。
Tips:可以使用连接点获取执行的类名、方法名和参数名等。
是在连接点的基础上来定义切入点。比如在一个类中,有 15 个方法,那么就会有几十个连接点,但只想让其中几个方法的前后或抛出异常时干点什么,那么就用切入点来定义这几个方法,让切入点来筛选连接点。
是通知(Advice)和切入点(Pointcut)的结合,通知(Advice)说明了干什么和什么时候(通过@Before、@Around、@After、@AfterReturning、@AfterThrowing来定义执行时间点)干,切入点(Pointcut)说明了在哪(指定方法)干,这就是一个完整的切面定义。
AOP Proxy:AOP 框架创建的对象,代理就是目标对象的加强。AOP 巧妙的例用 动态代理 优雅的解决了 OOP 力所不及的问题。Spring 中的 AOP 代理可以是 jdk 动态代理,也可以是 cglib 动态代理。前者基于接口,后者基于子类。
读完上面这么多抽象概念,如果不来一个 AOP 具体示例,吸收效果或者理解深度可能不是那么好。所以,请接着往下看:
import lombok.Data; @Data public class Result<T> { // code 状态值:0 代表成功,其他数值代表失败 private Integer code; // msg 返回信息。如果code为0,msg则为success;如果code为1,msg则为error private String msg; // data 返回结果集,使用泛型兼容不同的类型 private T data; }
import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor public enum ExceptionEnum { UNKNOW_ERROR(-1, "未知错误"), NULL_EXCEPTION(-2, "空指针异常:NullPointerException"), INVALID_EXCEPTION(1146, "无效的数据访问资源使用异常:InvalidDataAccessResourceUsageException"); public Integer code; public String msg; }
//@ControllerAdvice @Component @Slf4j public class ExceptionHandle { // @ExceptionHandler(value = Exception.class) // @ResponseBody public Result exceptionGet(Throwable t) { log.error("异常信息:", t); if (t instanceof InvalidDataAccessResourceUsageException) { return ResultUtil.error(ExceptionEnum.INVALID_EXCEPTION); } else if (t instanceof NullPointerException) { return ResultUtil.error(ExceptionEnum.NULL_EXCEPTION); } return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR); } }
制作一个结果返回工具类:
public class ResultUtil { /** * @return com.study.spring.entity.Result * @description 接口调用成功返回的数据格式 * @param: object */ public static Result success(Object object) { Result result = new Result(); result.setCode(0); result.setMsg("success"); result.setData(object); return result; } /** * @return com.study.spring.entity.Result * @description 接口调用失败返回的数据格式 * @param: code * @param: msg */ public static Result error(Integer code, String msg) { Result result = new Result(); result.setCode(code); result.setMsg(msg); result.setData(null); return result; } /** * 返回异常信息,在已知的范围内 * * @param exceptionEnum * @return */ public static Result error(ExceptionEnum exceptionEnum) { Result result = new Result(); result.setCode(exceptionEnum.code); result.setMsg(exceptionEnum.msg); result.setData(null); return result; } }
必须要添加 spring aop 等相关依赖:
<!-- web 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- aop 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 用于日志切面中,以 json 格式打印出入参 --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.5</version> </dependency> <!-- lombok 简化代码--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface HandleResult { String desc() default "create17"; }
上述代码内有些概念需要解释说明:
@Retention:定义注解的保留策略
@Target:定义注解的作用目标,可多个,用逗号分隔。
到这里,一个完整的自定义注解就定义完成了。
@Aspect @Component @Slf4j @Order(100) public class HandleResultAspect { ... }
使用 @Pointcut 来定义一个切点。
@Pointcut("@annotation(com.study.spring.annotation.HandleResult)") // @Pointcut("execution(* com.study.spring.controller..*.*(..))") public void HandleResult() { }
对于 execution 表达式,官网对 execution 表达式的介绍为:
除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。这个解释可能有点难理解,下面我们通过一个具体的例子来了解一下。在 HandleResultAspect 中我们定义了一个切点,其 execution 表达式为: * com.study.spring.controller..*.*(..))
,下表为该表达式比较通俗的解析:
标识符 | 含义 |
---|---|
execution() |
表达式的主体 |
第一个 * 符号 |
表示返回值的类型, * 代表所有返回类型 |
com.study.spring.controller |
AOP 所切的服务的包名,即需要进行横切的业务类 |
包名后面的 .. |
表示当前包及子包 |
第二个 * |
表示类名, * 表示所有类 |
最后的 .*(..) |
第一个 . 表示任何方法名,括号内为参数类型, .. 代表任何类型参数 |
上述的 execution 表达式是把 com.study.spring.controller 下所有的方法当作一个切点。@Pointcut 除了可以使用 execution 表达式之外,还可用 @annotation 来指定注解切入,比如可指定上面创建的自定义注解 @HandleResult ,@HandleResult 在哪里被使用,哪里就是一个切点。
@Before(value = "HandleResult() && @annotation(t)", argNames = "joinPoint,t") public void doBefore(JoinPoint joinPoint, HandleResult t) throws Exception { // 类名 String className = joinPoint.getTarget().getClass().getName(); // 方法名 String methodName = joinPoint.getSignature().getName(); // 参数名 Object[] args = joinPoint.getArgs(); StringBuilder sb = new StringBuilder(); if (args != null && args.length > 0) { for (Object arg : args) { sb.append(arg).append(", "); } } log.info("接口 {} 开始被调用, 类名: {}, 方法名: {}, 参数名为: {} .", t.desc(), className, methodName, sb.toString()); }
@Around("HandleResult()") public Result doAround(ProceedingJoinPoint point) { long startTime = System.currentTimeMillis(); log.info("---HandleResultAspect--Around的前半部分----------------------------"); Object result; try { // 执行切点。point.proceed 为方法返回值 result = point.proceed(); // 打印出参 log.info("接口原输出内容: {}", new Gson().toJson(result)); // 执行耗时 log.info("执行耗时:{} ms", System.currentTimeMillis() - startTime); return ResultUtil.success(result); } catch (Throwable throwable) { return exceptionHandle.exceptionGet(throwable); } }
@After("HandleResult()") public void doAfter() { log.info("doAfter..."); }
returning 可接收接口最终地返回信息。
@AfterReturning(pointcut = "@annotation(t)", returning = "res") public void afterReturn(HandleResult t, Object res) { log.info("接口 {} 被调用已结束, 最终返回结果为: {} .", t.desc(), new Gson().toJson(res)); }
throwing 可用来获取异常信息。
@AfterThrowing(throwing = "throwable", pointcut = "HandleResult()") public void afterThrowing(Throwable throwable) { log.info("After throwing...", throwable); }
关于这些通知的执行顺序如下图所示:
以下为切面实现的全部代码:
@Aspect @Component @Slf4j @Order(100) public class HandleResultAspect { @Autowired private ExceptionHandle exceptionHandle; /** * @return void * @description 定义切点 */ @Pointcut("@annotation(com.study.spring.annotation.HandleResult)") // @Pointcut("execution(* com.study.spring.controller..*.*(..))") public void HandleResult() { } /** * @return void * @description 打印接口名、类名、方法名及参数名 * @param: joinPoint * @param: t */ @Before(value = "@annotation(t)", argNames = "joinPoint,t") public void doBefore(JoinPoint joinPoint, HandleResult t) throws Exception { // 类名 String className = joinPoint.getTarget().getClass().getName(); // 方法名 String methodName = joinPoint.getSignature().getName(); // 参数名 Object[] args = joinPoint.getArgs(); StringBuilder sb = new StringBuilder(); if (args != null && args.length > 0) { for (Object arg : args) { sb.append(arg).append(", "); } } log.info("接口 {} 开始被调用, 类名: {}, 方法名: {}, 参数名为: {} .", t.desc(), className, methodName, sb.toString()); } /** * @return java.lang.Object * @description 定义@Around环绕,用于何时执行切点 * @param: proceedingJoinPoint */ @Around("HandleResult()") public Result doAround(ProceedingJoinPoint point) { long startTime = System.currentTimeMillis(); log.info("---HandleResultAspect--Around的前半部分----------------------------"); Object result; try { // 执行切点。point.proceed 为方法返回值 result = point.proceed(); // 打印出参 log.info("接口原输出内容: {}", new Gson().toJson(result)); // 执行耗时 log.info("执行耗时:{} ms", System.currentTimeMillis() - startTime); return ResultUtil.success(result); } catch (Throwable throwable) { return exceptionHandle.exceptionGet(throwable); } } /** * @return void * @description 程序无论正常还是异常,均执行的方法 * @param: */ @After("HandleResult()") public void doAfter() { log.info("doAfter..."); } /** * @return void * @description 当程序运行正常,所执行的方法 * 以json格式打印接口执行结果 * @param: t * @param: res */ @AfterReturning(pointcut = "@annotation(t)", returning = "res") public void afterReturn(HandleResult t, Object res) { log.info("接口 {} 被调用已结束, 接口最终返回结果为: {} .", t.desc(), new Gson().toJson(res)); } /** * @return void * @description 当程序运行异常,所执行的方法 * 可用来打印异常 * @param: throwable */ @AfterThrowing(throwing = "throwable", pointcut = "HandleResult()") public void afterThrowing(Throwable throwable) { log.info("After throwing...", throwable); } }
在生产中,我们的项目可能不止一个切面,那么在多切面的情况下,如何指定切面的优先级呢?
我们可以使用 @Order(i) 注解来定义切面的优先级,i 值越小,优先级越高。
比如我们再创建一个切面,代码示例如下:
@Aspect @Component @Order(50) @Slf4j public class TestAspect2 { @Pointcut("@annotation(com.study.spring.annotation.HandleResult)") public void aa(){ } @Before("aa()") public void bb(JoinPoint joinPoint){ log.info("我是 TestAspect2 的 Before 方法..."); } @Around("aa()") public Object cc(ProceedingJoinPoint point){ log.info("我是 TestAspect2 的 Around 方法的前半部分..."); Object result = null; try { result = point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } log.info("我是 TestAspect2 的 Around 方法的后半部分..."); return result; } @After("aa()") public void doAfter() { log.info("我是 TestAspect2 的 After 方法..."); } @AfterReturning("aa()") public void afterReturn() { log.info("我是 TestAspect2 的 AfterReturning 方法..."); } @AfterThrowing("aa()") public void afterThrowing() { log.info("我是 TestAspect2 的 AfterThrowing 方法..."); } }
切面 TestAspect2 为 @Order(50),之前的切面 HandleResultAspect 为 Order(100)。测试接口返回的日志如下图所示:
总结一下规律就是:
也就是: 先进后出 的原则。为了方便我们理解,我画了一个图,如下图所示:
一般在项目开发中,都会设置三个环境:开发、测试、生产。那么如果我只想在 开发 和 测试 环境下使用某切面该怎么办呢?我们只需要在指定的切面类上方加上注解 @Profile 就可以了,如下所示:
这样就指定了 HandleResultAspect 该切面只能在 dev(开发)环境、test(测试)环境下生效,prod(生产)环境不生效。当然,你需要创建相应的 application-${dev/test/prod}.yml 文件,最后在 application.yml 文件内指定 spring.profiles.active 属性为 dev 或 test 才可以生效。
本文篇幅较长,但总算对 Spring AOP 有了一个简单的了解。从 AOP 的起源到概念、使用场景,然后深入了解其专业术语,利用 AOP 思想实现了示例,方便我们自己理解。读完这篇文章,相信大家可以基本不惧面试官对这个知识点的考核了!
本文所涉及的代码已上传至 github :
https://github.com/841809077/...
本文参考链接: