spring注解驱动开发-(10)-AOP-业务类和切面类记录日志实例总结

1. AOP示例描述

我们模拟一个线上调用业务callData, 然后对callData做切面通知, 封装调用方法信息做日志记录的例子; 不过我们不持久化日志, 只做打印:

2. 注解AOP示例

step1: maven依赖

spring-aspects

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.7.RELEASE</version>
</dependency>
<!-- 我这里用到了gson包, 引入一下 -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.7</version>
</dependency>

会看到依赖包引入了:

spring-aspects-5.2.7.RELEASE.jar aspectjweaver-1.9.5.jar

step2: 业务逻辑类:

业务接口: BizRequest.java 和 依赖的 实体类 User.java

可见: 业务类只是调用一个 callData(Map<String, String> params)方法并返回 String结果(这里是User对象的json字符串);

如果业务参数 params里不包含全部的三要素参数, 会报异常:

package com.niewj.aop;

import com.google.gson.Gson;
import com.niewj.bean.User;

import java.util.Map;

public class BizRequest {

    /**
     * 请求远程数据: 假如 向某个接口请求一些数据, 然后返回一个用户信息;
     *
     * @param params 向第三方接口传递的参数
     * @return 第三方返回的数据:一般为json字符串
     */
    public String callData(Map<String, String> params) {
        if (params == null || !params.containsKey("idCard") || !params.containsKey("name") || !params.containsKey("phone")) {
            throw new IllegalArgumentException("name/idCard/phone三要素参数是必需的!");
        }

        User user = new User("niewj", "123456", true);
        return new Gson().toJson(user);
    }
}

User.java

package com.niewj.bean;

import lombok.Data;

@Data
public class User {
    private String name;
    private String passwd;
    private boolean online ;

    public User(String name, String passwd, boolean online){
        System.out.println("User-初始化!");
        this.name = name;
        this.passwd = passwd;
        this.online = online;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", passwd='" + passwd + '\'' +
                ", online=" + online +
                '}';
    }
}

step3: 定义切面类并注解(@Aspect):

通知类型包括:

1. 前置通知(@Before)

目标方法运行之前执行;

2. 后置通知(@After)

目标方法运行结束之后执行.

3. 返回通知(@AfterReturning)

目标方法正常返回之后执行.

4. 异常通知(@AfterThrowing)

目标方法出现异常之后执行.

5. 环绕通知(@Around)

动态代理, 手动推动目标方法运行: jointPoint.proceed();

定义一个切面类, 用来做切入逻辑: BizRequestLogAspects.java

package com.niewj.aop;

import com.google.gson.Gson;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class BizRequestLogAspects {

    /**
     * 下面的各个方法切入点医院, 可以抽取一个方法
     * 1. 方法名随意, 比如: myPointCut;
     * 2. 空实现即可;
     */
    @Pointcut("execution(public String com.niewj.aop.BizRequest.*(..))")
    public void myPointCut() {
    }

    @Before("myPointCut()") // 或者 外部用可以写全名: com.niewj.aop.BizRequestLogAspects.myPointCut()
    public void logStart(JoinPoint joinPoint) {
        long time = System.currentTimeMillis();
        String bizMethodName = joinPoint.getSignature().getName();
        String bizMethodParamJson = new Gson().toJson(joinPoint.getArgs());

        // 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
        // 以便将来提供消费查询服务
        LogInfo logInfo = new LogInfo();
        logInfo.setBizMethodName(bizMethodName);
        logInfo.setBizMethodParamsJson(bizMethodParamJson);
        logInfo.setLogTime(time);
        logInfo.setLogType(LogTypeEnum.BEFORE);
        System.out.println(logInfo);
    }

    @After(("myPointCut()"))
    public void logStop(JoinPoint joinPoint) {
        long time = System.currentTimeMillis();
        String bizMethodName = joinPoint.getSignature().getName();

        // 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
        // 以便将来提供消费查询服务
        LogInfo logInfo = new LogInfo();
        logInfo.setBizMethodName(bizMethodName);
        logInfo.setLogTime(time);
        logInfo.setLogType(LogTypeEnum.AFTER);
        System.out.println(logInfo);
    }

    @AfterReturning(value = "myPointCut()", returning = "rst")
    public void logReturn(JoinPoint joinPoint, Object rst) {
        long time = System.currentTimeMillis();
        String bizMethodName = joinPoint.getSignature().getName();
        String bizMethodParamJson = new Gson().toJson(joinPoint.getArgs());

        // 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
        // 以便将来提供消费查询服务
        LogInfo logInfo = new LogInfo();
        logInfo.setBizMethodName(bizMethodName);
        logInfo.setBizMethodParamsJson(bizMethodParamJson);
        logInfo.setLogTime(time);
        logInfo.setLogType(LogTypeEnum.AFTER_RETURNING);
        logInfo.setReturnJson(String.valueOf(rst));
        System.out.println(logInfo);
    }

    @AfterThrowing(value = "myPointCut()", throwing = "ex")
    public void logException(JoinPoint joinPoint, Exception ex) {
        long time = System.currentTimeMillis();
        String bizMethodName = joinPoint.getSignature().getName();
        String bizMethodParamJson = new Gson().toJson(joinPoint.getArgs());

        // 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
        // 以便将来提供消费查询服务
        LogInfo logInfo = new LogInfo();
        logInfo.setBizMethodName(bizMethodName);
        logInfo.setBizMethodParamsJson(bizMethodParamJson);
        logInfo.setLogTime(time);
        logInfo.setLogType(LogTypeEnum.AFTER_THROWING);
        logInfo.setException(ex);
        System.out.println(logInfo);
    }
}

封装的 LogInfo日志类:

package com.niewj.aop;

import com.google.gson.Gson;
import lombok.Data;

@Data
public class LogInfo {
    private String bizMethodName;
    private String bizMethodParamsJson;
    private long logTime;
    private LogTypeEnum logType;
    private Exception exception;
    private String returnJson;

    @Override
    public String toString() {
        return "[" + this.logType.getValue() + "]==>\t" + new Gson().toJson(this);
    }
}

顺便声明了个LogTypeEnum枚举类:

package com.niewj.aop;

public enum LogTypeEnum {
    BEFORE("@Before"),
    AFTER("@After"),
    AFTER_RETURNING("@AfterReturning"),
    AFTER_THROWING("@AfterThrowing"),
    AROUND("@Around");

    private String value;

    LogTypeEnum(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

6. 切面类注意小结:

1. @Aspect注解

配置类里只是注解了@Bean, 需要这里的@Aspect标注, 容器才会辨别它就是切面类;

此注解相当于开启aspectj自动代理设置–> 类似于之前xml配置:

<aop:aspectj-autoproxy />

2. @Pointcut 的方法

只是对公共的切入点做抽取, 方便下面的各个类型的通知方法引用;

PointCut的方法名可以随意取, 下面引用对即可: 如果是外部引用, 需要全路径的方法签名字符串! 本类中 简写的即可!

3. 方法加参数JoinPoint必须是第一个;

4. JointPoint类常用方法

获取业务方法名: String bizMethodName = joinPoint.getSignature().getName();
获取业务方法入参: Object[] params = joinPoint.getArgs();

获取方法返回值, 需要 (1). @AfterReturning指定属性returning名, (2). 然后加到参数列表里, 在处理方法里调用即可!

获取方法异常, 需要 (1). @AfterThrowing指定属性throwing名, (2). 然后加到参数列表里, 在处理方法里调用即可!

step4: 业务类+切面类-注册到spring容器

1. @Bean 注册业务类到容器;

2. @Bean 注册切面类到容器;

3. @EnableAspectJAutoProxy 开启自动代理

配置类: AspectsConfig.java

package com.niewj.config;

import com.niewj.aop.BizRequest;
import com.niewj.aop.BizRequestLogAspects;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AspectsConfig {

    @Bean
    public BizRequest bizRequest() {
        return new BizRequest();
    }

    @Bean
    public BizRequestLogAspects bizRequestLogAspects() {
        return new BizRequestLogAspects();
    }
}

测试用例1: 正常返回

@Test
public void testAopReturning() {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectsConfig.class);

    // 打印spring容器中的 BeanDefinition
    Stream.of(ctx.getBeanDefinitionNames()).forEach(e -> System.out.println(e));
    System.out.println("=============================");

    // 正常返回的用例:
    BizRequest bizRequest = ctx.getBean(BizRequest.class);
    Map<String, String> params = new HashMap<>();
    params.put("name", "niewj");
    params.put("idCard", "142525199905051111");
    params.put("phone", "15215152525");

    bizRequest.callData(params);
    ctx.close();
}

output: 主要输出: 11-14行

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
aspectsConfig
bizRequest
bizRequestLogAspects
org.springframework.aop.config.internalAutoProxyCreator
=============================
[@Before]==>    {"bizMethodName":"callData","bizMethodParamsJson":"[{\"phone\":\"15215152525\",\"idCard\":\"142525199905051111\",\"name\":\"niewj\"}]","logTime":1595004597856,"logType":"BEFORE"}
User-初始化!
[@AfterReturning]==>    {"bizMethodName":"callData","bizMethodParamsJson":"[{\"phone\":\"15215152525\",\"idCard\":\"142525199905051111\",\"name\":\"niewj\"}]","logTime":1595004597922,"logType":"AFTER_RETURNING","returnJson":"{\"name\":\"niewj\",\"passwd\":\"123456\",\"online\":true}"}
[@After]==>    {"bizMethodName":"callData","logTime":1595004597924,"logType":"AFTER"}

测试用例2: 异常返回:

@Test
public void testAopThrowing() {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectsConfig.class);

    // 打印spring容器中的 BeanDefinition
    Stream.of(ctx.getBeanDefinitionNames()).forEach(e -> System.out.println(e));
    System.out.println("=============================");

    // 正常返回的用例:
    BizRequest bizRequest = ctx.getBean(BizRequest.class);
    Map<String, String> leakParams = new HashMap<>();
    leakParams.put("name", "niewj");

    bizRequest.callData(leakParams);
    ctx.close();
}

output: 11-15行

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
aspectsConfig
bizRequest
bizRequestLogAspects
org.springframework.aop.config.internalAutoProxyCreator
=============================
[@Before]==>    {"bizMethodName":"callData","bizMethodParamsJson":"[{\"name\":\"niewj\"}]","logTime":1595004648830,"logType":"BEFORE"}
[@AfterThrowing]==>    {"bizMethodName":"callData","bizMethodParamsJson":"[{\"name\":\"niewj\"}]","logTime":1595004648908,"logType":"AFTER_THROWING","exception":{"detailMessage":"name/idCard/phone三要素参数是必需的!","stackTrace":[],"suppressedExceptions":[]}}
[@After]==>    {"bizMethodName":"callData","logTime":1595004648910,"logType":"AFTER"}

java.lang.IllegalArgumentException: name/idCard/phone三要素参数是必需的!

    at com.niewj.aop.BizRequest.callData(BizRequest.java:18)
    at com.niewj.aop.BizRequest$$FastClassBySpringCGLIB$$b4cd2786.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    ..................

3. AOP小结:

主要步骤

1. 业务类(注册到容器) :@Bean

2. 切面类(标注切面+注册到容器): @Bean+@Aspect

3. 配置类(开启AspectJ自动代理) @EnableAspectJAutoProxy


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hi@niewj.com

×

喜欢就点赞,疼爱就打赏