目的:
統一日誌輸出格式
思路:
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. 工程結構