Java研究院 Java研究院

人非生而知之者,知之,而后知不知。

目录
Spring AOP 原理详解及实践
/    

Spring AOP 原理详解及实践

什么是AOP?

AOP 是一种 编程思想 ,是面向对象编程(OOP)的一种补充。
面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

切面须是"横切",剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

为什么需要 AOP

编程开发人员随着业务的迭代,会发现业务会变得越来越复杂,原本很清晰明了的业务,会随着需求点的增加而将整体的业务代码修改得支离破碎。
一般来说,熟练的开发人员会把比较核心的完整功能单独拆分,尽量避免去改动。这样做不仅能避免业务不熟悉导致将代码改坏,也便于在后期维护的时候,能尽可能小的做局部调整。

而 AOP 的存在,则能为那些不能重用,又基本相同的业务逻辑,提供一个公共处理的入口。通过 AOP 切面收敛的方式,能统一去做一些业务层面的扩展和拦截。
比如:统一请求的出入参处理、统一方法级别的日志处理。

应用场景

AOP 的存在,为应用解耦提供了很多可行性的操作空间,大概有一下的应用场景:

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging 调试
  • Log 日志记录跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence 持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

核心概念

  • 1、切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象;
  • 2、横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。;
  • 3、连接点(join point):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。;
  • 4、切入点(pointcut):对连接点进行拦截的定义;
  • 5、通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类;
  • 6、目标对象:代理的目标对象;
  • 7、织入(weave):将切面应用到目标对象并导致代理对象创建的过程;
  • 8、引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。

AOP 的实现

AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。
AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,按照 AOP 框架修改源代码的时机,可以将其分为两类:

  • 静态 AOP 实现:AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
  • 动态 AOP 实现:AOP 框架在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。

AOP 实现

Spring 提供了两种方式来生成代理对象: JDKProxy 和 Cglib,具体使用哪种方式生成由 AopProxyFactory 根据 AdvisedSupport 对象的配置来决定。
默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用 Cglib 来生成代理。

JDK 动态代理

JDK 动态代理主要涉及到 java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。 InvocationHandler是一个接口,通过实现该接口定义横切逻辑,
并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象。

CGLib 动态代理

CGLib 全称为 Code Generation Library,是一个强大的高性能,高质量的代码生成类库,可以在运行期扩展 Java 类与实现 Java 接口,
CGLib 封装了 asm,可以再运行期动态生成新的 class。
和 JDK 动态代理相比较:JDK 创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过 CGLib 创建动态代理。

简单实现

此处基于 Spring-Boot 实现一个自定义业务日志处理为例,采用自定义注解的方式来说明。

引入依赖

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <optional>true</optional>
</dependency>

### 创建注解类

通过  **`@interface`** 申明一个注解 **BizLog** 。

```java
@Target({ElementType.METHOD})//目标作用于方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BizLog {
  
    /**
     * 日志描述
     *
     * @return
     */
    String title() default "";
  
    /**
     * orderId提取表达式
     *
     * @return
     */
    String orderId() default "";
    String mobile() default "";
    String userNo() default "";
  
    /**
     * 结果处理类
     */
    Class<?> resultHandle() default ResultHandler.class;
  
    /**
     * 入参处理类
     */
    Class<?> paramsHandle() default ParamsHandler.class;
  
    /**
     * 结果处理方法,设置结果处理类时有效
     *
     * @return
     */
    String resultMethod() default "handle";
  
    /**
     * 入参处理方法,设置入参处理类时有效
     *
     * @return
     */
    String paramsMethod() default "handle";
  
    /**
     * 是否处理响应结果
     *
     * @return
     */
    boolean result() default true;
  
    /**
     * 是否处理入参
     *
     * @return
     */
    boolean request() default true;
  
    /**
     * 入参、结果 SpELl表达式解析
     *
     * @return
     */
    String builder() default "";
  
    long delay() default 0;
}

创建切面类

@Slf4j
@Aspect
@Component
@EnableAsync
public class BizLogAspect {
  
    @Autowired
    AsyncBizLogService asyncBizLogService;
  
    //切点,即引用了 BizLog 注解的方法,会被当前切面拦截处理
    @Pointcut("@annotation(icu.studying.log.annotation.BizLog)")
    public void pointCut() {
  
    }
  
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
    
        // RawLog 是一天日志记录对象,此处切面拦截一个到一个方法后,会将一些信息存入,便于后续处理。
        RawLog rawLog = new RawLog();
        rawLog.setLogTime(LocalDateTime.now());
        rawLog.setCreateTime(LocalDateTime.now());
        UserVo userVo = SessionHelper.getUserVo();
        if (userVo != null) {
            rawLog.setUserNo(userVo.getAccountId());
            rawLog.setMobile(userVo.getMobile());
        }
        long start = System.currentTimeMillis();
        Handler handler = new Handler(getAnnotationLog(joinPoint), joinPoint.getArgs(), getParamNameValueMap(joinPoint), rawLog);
        try {
            Object result = joinPoint.proceed();
            long end = System.currentTimeMillis() - start;
            rawLog.setCostTime(end);
            handler.setResult(result);
            // AsyncBizLogService 中,定义了业务日志的处理逻辑,以及日志打印、入库 或 通过mq广播出去
            // BizLog 中的 resultHandle()  和 paramsHandle() 会进一步对日志记录做加强处理
            asyncBizLogService.handle(handler);
            return result;
        } catch (BizException e) {
            long end = System.currentTimeMillis() - start;
            rawLog.setCostTime(end);
            handler.setException(e);
            asyncBizLogService.handle(handler);
            log.error("bizError:",e);
            throw e;
        } catch (Throwable e) {
            long end = System.currentTimeMillis() - start;
            rawLog.setCostTime(end);
            handler.setException(new Exception(e));
            asyncBizLogService.handle(handler);
            log.error("bizError:",e);
            throw new RuntimeException(e.getMessage());
        }
    }
  
    private String[] getParamNameValueMap(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] methodParameters = methodSignature.getParameterNames();
        return methodParameters;
    }
  
    private BizLog getAnnotationLog(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null) {
            return method.getAnnotation(BizLog.class);
        }
        return null;
    }
}

注解使用

@BizLog(title = "叫车",
            orderId = "{order.basicOrderId}",
            userNo = "{order.userNo}",
            mobile = "{order.passengerPhone}",
            paramsHandle = ParamsHandle.class,
            resultHandle = ResultHandle.class)
public Object xxx(Order order){}

该注解使用时,指定了日志标题类型为用车,并且通过 order 入参,借助 spel 解析了一些属性。
同时,ParamsHandle 和 ResultHandle 是可以自定义的,通过集成实现自定义日志的解析和封装。

实际效果

因为样子截取自公司自研组件,通过 SpringBootStarer的方式集成,且通过mq异步发送并处理的。这里就简单截图说明。

日志列表

通过日志列表,可以看到上述使用样例申明的字段大多涵盖了。

叫车日志详情

这是一条叫车的日志详情,其中具体预估了哪些车辆,勾选叫车的详情,从哪出发到哪里去,以及详细调价的情况、风控信息,都是通过 ParamsHandle、ResultHandle 来处理的。

如果对这个业务日志实现详情感兴趣的,可以留言交流。



才疏学浅,难免有坑,有坑也不填
标题:Spring AOP 原理详解及实践
地址:https://studying.icu/articles/2021/09/18/1631948394690.html