目的:
统一日志输出格式
思路:
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>