• Spring AOP实现统一日志输出


    目的:

    统一日志输出格式

    思路:

    1、针对不同的调用场景定义不同的注解,目前想的是接口层和服务层。

    2、我设想的接口层和服务层的区别在于:

      (1)接口层可以打印客户端IP,而服务层不需要

      (2)接口层的异常需要统一处理并返回,而服务层的异常只需要向上抛出即可

    3、就像Spring中的@Controller、@Service、@Repository注解那样,虽然作用是一样的,但是不同的注解用在不同的地方显得很清晰,层次感一下就出来了

    4、AOP去拦截特定注解的方法调用

    5、为了简化使用者的操作,采用Spring Boot自动配置

    1. 注解定义

    package com.cjs.example.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface SystemControllerLog {
    
        String description() default "";
    
        boolean async() default false;
    
    }
    package com.cjs.example.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface SystemServiceLog {
    
        String description() default "";
    
        boolean async() default false;
    
    }

    2. 定义一个类包含所有需要输出的字段

    package com.cjs.example.service;
    
    import lombok.Data;
    import java.io.Serializable;
    
    @Data
    public class SystemLogStrategy implements Serializable {
    
        private boolean async;
    
        private String threadId;
    
        private String location;
    
        private String description;
    
        private String className;
    
        private String methodName;
    
        private String arguments;
    
        private String result;
    
        private Long elapsedTime;
    
    
        public String format() {
            return "线程ID: {}, 注解位置: {}, 方法描述: {}, 目标类名: {}, 目标方法: {}, 调用参数: {}, 返回结果: {}, 花费时间: {}";
        }
    
        public Object[] args() {
            return new Object[]{this.threadId, this.location, this.description, this.className, this.methodName, this.arguments, this.result, this.elapsedTime};
        }
    
    }

    3. 定义切面

    package com.cjs.example.aspect;
    
    import com.alibaba.fastjson.JSON;
    import com.cjs.example.annotation.SystemControllerLog;
    import com.cjs.example.annotation.SystemRpcLog;
    import com.cjs.example.annotation.SystemServiceLog;
    import com.cjs.example.enums.AnnotationTypeEnum;
    import com.cjs.example.service.SystemLogStrategy;
    import com.cjs.example.util.JsonUtil;
    import com.cjs.example.util.ThreadUtil;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.lang.reflect.Method;
    
    @Aspect
    public class SystemLogAspect {
    
        private static final Logger LOG = LoggerFactory.getLogger(SystemLogAspect.class);
    
        private static final Logger LOG = LoggerFactory.getLogger(SystemLogAspect.class);
    
        @Pointcut("execution(* com.ourhours..*(..)) && !execution(* com.ourhours.logging..*(..))")
        public void pointcut() {
    
        }
    
        @Around("pointcut()")
        public Object doInvoke(ProceedingJoinPoint pjp) {
            long start = System.currentTimeMillis();
    
            Object result = null;
    
            try {
                result = pjp.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                LOG.error(throwable.getMessage(), throwable);
                throw new RuntimeException(throwable);
            } finally {
                long end = System.currentTimeMillis();
                long elapsedTime = end - start;
    
                printLog(pjp, result, elapsedTime);
    
            }
    
            return result;
        }
    
        /**
         * 打印日志
         * @param pjp   连接点
         * @param result    方法调用返回结果
         * @param elapsedTime   方法调用花费时间
         */
        private void printLog(ProceedingJoinPoint pjp, Object result, long elapsedTime) {
            SystemLogStrategy strategy = getFocus(pjp);
    
            if (null != strategy) {
                strategy.setThreadId(ThreadUtil.getThreadId());
                strategy.setResult(JsonUtil.toJSONString(result));
                strategy.setElapsedTime(elapsedTime);
                if (strategy.isAsync()) {
                    new Thread(()->LOG.info(strategy.format(), strategy.args())).start();
                }else {
                    LOG.info(strategy.format(), strategy.args());
                }
            }
        }
    
        /**
         * 获取注解
         */
        private SystemLogStrategy getFocus(ProceedingJoinPoint pjp) {
            Signature signature = pjp.getSignature();
            String className = signature.getDeclaringTypeName();
            String methodName = signature.getName();
            Object[] args = pjp.getArgs();
            String targetClassName = pjp.getTarget().getClass().getName();
            try {
                Class<?> clazz = Class.forName(targetClassName);
                Method[] methods = clazz.getMethods();
                for (Method method : methods) {
                    if (methodName.equals(method.getName())) {
                        if (args.length == method.getParameterCount()) {
    
                            SystemLogStrategy strategy = new SystemLogStrategy();
                            strategy.setClassName(className);
                            strategy.setMethodName(methodName);
    
                            SystemControllerLog systemControllerLog = method.getAnnotation(SystemControllerLog.class);
                            if (null != systemControllerLog) {
                                strategy.setArguments(JsonUtil.toJSONString(args));
                                strategy.setDescription(systemControllerLog.description());
                                strategy.setAsync(systemControllerLog.async());
                                strategy.setLocation(AnnotationTypeEnum.CONTROLLER.getName());
                                return strategy;
                            }
                            SystemServiceLog systemServiceLog = method.getAnnotation(SystemServiceLog.class);
                            if (null != systemServiceLog) {
                                strategy.setArguments(JsonUtil.toJSONString(args));
                                strategy.setDescription(systemServiceLog.description());
                                strategy.setAsync(systemServiceLog.async());
                                strategy.setLocation(AnnotationTypeEnum.SERVICE.getName());
                                return strategy;
                            }
                            
                            return null;
                        }
                    }
                }
            } catch (ClassNotFoundException e) {
                LOG.error(e.getMessage(), e);
            }
            return null;
        }
    
    }

    4. 配置

    PS:

    这里也可以用组件扫描,执行在Aspect上加@Component注解即可,但是这样的话有个问题。

    就是,如果你的这个Aspect所在包不是Spring Boot启动类所在的包或者子包下就需要指定@ComponentScan,因为Spring Boot默认只扫描和启动类同一级或者下一级包。

    package com.cjs.example.config;
    
    import com.cjs.example.aspect.SystemLogAspect;
    import org.springframework.boot.autoconfigure.AutoConfigureOrder;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @AutoConfigureOrder(2147483647)
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    @ConditionalOnClass(SystemLogAspect.class)
    @ConditionalOnMissingBean(SystemLogAspect.class)
    public class SystemLogAutoConfiguration {
    
        @Bean
        public SystemLogAspect systemLogAspect() {
            return new SystemLogAspect();
        }
    }

    5. 自动配置(resources/META-INF/spring.factories)

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ourhours.logging.config.SystemLogAutoConfiguration

    6. 其它工具类

    6.1. 获取客户端IP

    package com.cjs.example.util;
    
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    
    public class HttpContextUtils {
    
        public static HttpServletRequest getHttpServletRequest() {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            return servletRequestAttributes.getRequest();
        }
    
        public static String getIpAddress() {
            HttpServletRequest request = getHttpServletRequest();
            String ip = request.getHeader("X-Forwarded-For");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                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();
                }
            }else if (ip != null && ip.length() > 15) {
                String[] ips = ip.split(",");
                for (int index = 0; index < ips.length; index++) {
                    String strIp = (String) ips[index];
                    if (!("unknown".equalsIgnoreCase(strIp))) {
                        ip = strIp;
                        break;
                    }
                }
            }
            return ip;
        }
    }

    6.2. 格式化成JSON字符串

    package com.cjs.example.util;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.serializer.SerializerFeature;
    
    public class JsonUtil {
    
        public static String toJSONString(Object object) {
            return JSON.toJSONString(object, SerializerFeature.DisableCircularReferenceDetect);
        }
    
    }

    6.3. 存取线程ID

    package com.cjs.example.util;
    
    import java.util.UUID;
    
    public class ThreadUtil {
    
        private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        public static String getThreadId() {
            String threadId = threadLocal.get();
            if (null == threadId) {
                threadId = UUID.randomUUID().toString();
                threadLocal.set(threadId);
            }
            return threadId;
        }
    
    }

    7. 同时还提供静态方法

    package com.cjs.example;
    
    import com.cjs.example.util.JsonUtil;
    import com.cjs.example.util.ThreadUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class Log {
    
        private static Logger LOGGER = null;
    
        private static class SingletonHolder{
            public static Log instance = new Log();
        }
    
        private Log(){}
    
        public static Log getInstance(Class<?> clazz){
            LOGGER = LoggerFactory.getLogger(clazz);
            return SingletonHolder.instance;
        }
    
        public void info(String description, Object args, Object result) {
            LOGGER.info("线程ID: {}, 方法描述: {}, 调用参数: {}, 返回结果: {}", ThreadUtil.getThreadId(), description, JsonUtil.toJSONString(args), JsonUtil.toJSONString(result));
        }
    
        public void error(String description, Object args, Object result, Throwable t) {
            LOGGER.error("线程ID: {}, 方法描述: {}, 调用参数: {}, 返回结果: {}", ThreadUtil.getThreadId(), description, JsonUtil.toJSONString(args), JsonUtil.toJSONString(result), t);
        }
    
    }

    8. pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.cjs.example</groupId>
        <artifactId>cjs-logging</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <name>cjs-logging</name>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.2.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <java.version>1.8</java.version>
            <aspectj.version>1.8.13</aspectj.version>
            <servlet.version>4.0.0</servlet.version>
            <slf4j.version>1.7.25</slf4j.version>
            <fastjson.version>1.2.47</fastjson.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-autoconfigure</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-web</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>${servlet.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>${aspectj.version}</version>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>${slf4j.version}</version>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
                <optional>true</optional>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.7.0</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>UTF8</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>attach-sources</id>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    
    </project>

    8. 工程结构

  • 相关阅读:
    孤儿进程与僵尸进程
    python with as的用法
    工作目录与os.getcwd()
    内置模块
    迭代器,生成器
    表达式,语句
    字符流
    字节流
    File
    触发器的操作
  • 原文地址:https://www.cnblogs.com/cjsblog/p/9111975.html
Copyright © 2020-2023  润新知