Spring Boot利用AOP实现日志记录

Posted by Kaka Blog on November 18, 2020

利用swagger注解,实现日志打印,通过配置文件配置属性可写入到数据库。

创建maven项目

<groupId>com.fang</groupId>
<artifactId>system-log</artifactId>
<version>1.0</version>

pom.xml文件

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.1.7.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.17</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>5.1.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <version>2.1.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.54</version>
    </dependency>
    <dependency>
        <groupId>com.battcn</groupId>
        <artifactId>swagger-spring-boot-starter</artifactId>
        <version>2.1.5-RELEASE</version>
    </dependency>
</dependencies>

实体类

@Data
@Builder
@TableName("sys_log")
public class OperLog {
    private String id;
    private String operModule;
    private String operDesc;
    private String operMethod;
    private String operRequstParam;
    private String operResponse;
    private String operUserName;
    private String operIp;
    private Timestamp operTime;
    private String operUri;
    private String operVersion;
}

实现日志AOP类

@Slf4j
@Aspect
@Component
public class OperLogAspect {
    @Value("${project.version}")
    private String operVersion;

    @Value("${log.database.open:false}")
    private boolean log2Database;

    @Autowired
    private SysLogMapper sysLogMapper;

    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    @Pointcut("@annotation(io.swagger.annotations.ApiOperation)")
    public void operLogPointCut() {

    }

    @AfterReturning(value = "operLogPointCut()", returning = "keys")
    public void saveOperLog(JoinPoint joinPoint, Object keys) {
        log.info("进入日志处理" + keys);
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
        if (StringUtils.isEmpty(apiOperation.notes())) {
            return;
        }
        Api api = joinPoint.getTarget().getClass().getAnnotation(Api.class);
        String className = joinPoint.getTarget().getClass().getName();
        Map<String, String> paramMap = convertMap(request.getParameterMap());
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = null;
        if (authentication != null && authentication.isAuthenticated()) {
            username = authentication.getName();
        }
        final OperLog operLogBuilder = OperLog.builder().id(UUID.randomUUID().toString())
                .operModule(api.value())
                .operDesc(apiOperation.notes())
                .operMethod(className + "." + method.getName())
                .operRequstParam(JSON.toJSONString(paramMap))
                .operUri(request.getRequestURI())
                .operTime(new Timestamp(System.currentTimeMillis()))
                .operVersion(operVersion)
                .operResponse(JSON.toJSONString(keys))
                .operUserName(username)
                .operIp(IpUtil.getIpAddress(request))
                .build();
        log.info(JSON.toJSONString(operLogBuilder));
        if (log2Database && sysLogMapper != null) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    sysLogMapper.insert(operLogBuilder);
                }
            });
        }
    }

    private Map<String, String> convertMap(Map<String, String[]> parameterMap) {
        Map<String, String> paramMap = new HashMap<String, String>();
        for (String key : parameterMap.keySet()) {
            paramMap.put(key, parameterMap.get(key)[0]);
        }
        return paramMap;
    }
}

IP工具类

public class IpUtil {
    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     *
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     *
     * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
     * 192.168.1.100
     *
     * 用户真实IP为: 192.168.1.110
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Real-IP");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip.equals("0:0:0:0:0:0:0:1")?"127.0.0.1":ip;
    }
}

Java配置类

@Configuration
@ComponentScan
public class JavaConfig {
}

spring.factories文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.fang.JavaConfig

日记记录Mapper类

public interface SysLogMapper extends BaseMapper<OperLog> {
}

日志表SQL

DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
  `id` varchar(64) NOT NULL,
  `oper_module` varchar(100) DEFAULT NULL COMMENT '操作模块',
  `oper_desc` varchar(100) DEFAULT NULL COMMENT '操作描述',
  `oper_method` varchar(100) DEFAULT NULL COMMENT '操作方法',
  `oper_requst_param` varchar(255) DEFAULT NULL COMMENT '操作参数',
  `oper_response` varchar(255) DEFAULT NULL COMMENT '响应内容',
  `oper_username` varchar(100) DEFAULT NULL COMMENT '操作用户',
  `oper_ip` varchar(32) DEFAULT NULL COMMENT '操作IP',
  `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
  `oper_uri` varchar(100) DEFAULT NULL COMMENT '操作路径',
  `oper_version` varchar(32) DEFAULT NULL COMMENT '操作版本',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

使用方法

pom.xml添加依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.17</version>
</dependency>
<dependency>
    <groupId>com.fang</groupId>
    <artifactId>system-log</artifactId>
    <version>1.0</version>
</dependency>

修改配置文件

spring.main.allow-bean-definition-overriding=true
project.version=@project.version@
log.database.open=true
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  • project.version: 应用版本号,对应pom文件里的版本
  • log.database.open:是否记录到数据库,默认为false

启动类添加注解

@MapperScan("com.fang.mapper")

controller添加swagger注解

@Api(value = "订单管理")
@ApiOperation(value = "计算金额", notes = "计算金额")
  • @Api的value属性当做操作模块
  • @ApiOperation的notes数据当做操作描述,如果没有notes属性,则不会记录日志

测试

在浏览器访问接口,可以看到在控制台打印信息,数据库有新增数据。

img

总结

@Aspect用于实现Spring AOP,可以在被调用的方法执行前、执行后增加自定义的逻辑。

拦截方向:

Filter -> Interceptor -> ControllerAdvice -> Aspect -> Controller

所以用ControllerAdvice对结果进行处理后,Aspect是拿不到处理后的结果。