aop 打印 http 日志
本文项目已发布到 github,后续学习项目也会添加到此工程下,欢迎 fork 点赞。
https://github.com/wangyuheng/spring-boot-sample
需要具备少量 aop 基础,通过 springboot 构建项目方便演示。
AOP-面向切面编程
一句话描述,在 java 对象增加切点,在不改变对象的前提下通过代理扩展功能。
http 日志打印拦截器
restful api
通过 springboot 快速搭建一个 RestController 接口。
import org.springframework.web.bind.annotation.*;
import wang.crick.study.httplog.annotation.HttpLog;
import wang.crick.study.httplog.domain.User;
import java.util.Random;
@RestController
@RequestMapping("/user")
public class UserApi {
@GetMapping("/log/{id}")
public RestApiResponse<User> getInfo(@PathVariable("id") int id,
@RequestParam("age") int age){
User user = new User();
user.setId(id);
user.setUsername(String.valueOf(new Random().nextLong()));
user.setAge(age);
return RestApiResponse.success(user);
}
@GetMapping("/log/pwd/{id}")
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestHeader("username") String username,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
return RestApiResponse.success(user);
}
@GetMapping("/log/pwdExcludeResponse/{id}")
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestParam("age") int age,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setPassword(password);
user.setAge(age);
return RestApiResponse.success(user);
}
}
切面选择
一般教程会选择拦截所有 http 请求,并打印 request.parameters。但是存在问题:
- 不够灵活,部分参数不想打印,如文件数据(过大)、敏感数据(身份证)等。
- 显式的标注日志输出,避免给维护人员造成疑惑。
- 部分参数通过 header 传输
因此,自定义日志输出 @annotation HttpLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpLog {
/**
* 忽略参数,避免文件or无意义参数打印
*
* @return 忽略参数数组
*/
String[] exclude() default {};
/**
* 需要打印的header参数
*
* @return header参数名数组
*/
String[] headerParams() default {};
boolean ignoreResponse() default false;
}
获取 HttpServletRequest
spring 通过 ThreadLocal 持有 request 参数。
private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}
获取 uri
根据拦截规则不同,getServletPath()和 request.getPathInfo()可能为空,简单的做一次健壮性判断。
private String getRequestPath(HttpServletRequest request) {
return (null != request.getServletPath() && request.getServletPath().length() > 0)
? request.getServletPath() : request.getPathInfo();
}
aop 日志输出
- Pointcut 自定义 @annotation HttpLog
- 拿到 @annotation,读取自定义属性,如忽略 response、打印 headers 等
- 遍历 request & headers 中需打印参数
- 定制日志格式并打印 log
- 拦截返回值并打印 log
Aspect 代码如下
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import wang.crick.study.httplog.annotation.HttpLog;
import wang.crick.study.httplog.api.RestApiResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Aspect
public class HttpLogAspect {
private Logger log = LoggerFactory.getLogger(HttpLogAspect.class);
@Pointcut("@annotation(wang.crick.study.httplog.annotation.HttpLog)")
public void logAnnotation() {
}
private Optional<HttpLog> getLogAnnotation(JoinPoint joinPoint) {
if (joinPoint instanceof MethodInvocationProceedingJoinPoint) {
Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(HttpLog.class)) {
return Optional.of(method.getAnnotation(HttpLog.class));
}
}
}
return Optional.empty();
}
private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}
private String getRequestPath(HttpServletRequest request) {
return (null != request.getServletPath() && request.getServletPath().length() > 0)
? request.getServletPath() : request.getPathInfo();
}
@Before("logAnnotation()")
public void requestLog(JoinPoint joinPoint) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
HttpServletRequest request = getRequest();
List<String> excludes = Arrays.asList(anno.exclude());
List<Object> params = new ArrayList<>();
StringBuilder logMsg = new StringBuilder();
logMsg.append("REQUEST_LOG. sessionId:{}. ")
.append("requestUrl: ")
.append(getRequestPath(request))
.append(" -PARAMS- ");
params.add(request.getSession().getId());
request.getParameterMap().forEach((k, v) -> {
if (!excludes.contains(k)) {
logMsg.append(k).append(": {}, ");
params.add(v);
}
});
if (anno.headerParams().length > 0) {
logMsg.append(" -HEADER_PARAMS- ");
Arrays.asList(anno.headerParams()).forEach(param -> {
logMsg.append(param).append(": {}, ");
params.add(request.getHeader(param));
});
}
log.info(logMsg.toString(), params.toArray());
});
} catch (Exception ignore) {
log.warn("print request log fail!", ignore);
}
}
@AfterReturning(returning = "restApiResponse", pointcut = "logAnnotation()")
public void response(JoinPoint joinPoint, RestApiResponse restApiResponse) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
if (!anno.ignoreResponse()) {
log.info("RESPONSE_LOG. sessionId:{}. result:{}", getRequest().getSession().getId(), restApiResponse);
}
});
} catch (Exception ignore) {
log.warn("print response log fail!", ignore);
}
}
}
使用
在 RestController 中增加自定义注解 HttpLog
import org.springframework.web.bind.annotation.*;
import wang.crick.study.httplog.annotation.HttpLog;
import wang.crick.study.httplog.domain.User;
import java.util.Random;
@RestController
@RequestMapping("/user")
public class UserApi {
/**
* curl -H 'username:12b4' -H 'password:34ndd' -v 'http://localhost:8080/user/log/123?age=32'
*/
@GetMapping("/log/{id}")
@HttpLog()
public RestApiResponse<User> getInfo(@PathVariable("id") int id,
@RequestParam("age") int age){
User user = new User();
user.setId(id);
user.setUsername(String.valueOf(new Random().nextLong()));
user.setAge(age);
return RestApiResponse.success(user);
}
/**
* curl -H 'username:12b4' -H 'password:34ndd' -v 'http://localhost:8080/user/log/pwd/123?age=32'
*/
@GetMapping("/log/pwd/{id}")
@HttpLog(headerParams="password")
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestHeader("username") String username,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
return RestApiResponse.success(user);
}
/**
* curl -H 'username:12b4' -H 'password:34ndd' -v 'http://localhost:8080/user/log/pwdExcludeResponse/123?age=32'
*/
@GetMapping("/log/pwdExcludeResponse/{id}")
@HttpLog(headerParams="username", ignoreResponse = true)
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestParam("age") int age,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setPassword(password);
user.setAge(age);
return RestApiResponse.success(user);
}
}
加载 HttpLogAspect 对象
可以在 @SpringBootApplication 类下直接加在,也可以在 HttpLogAspect 中增加 @Component 注解,推荐前者,更清晰。不需要增加 @EnableAspectJAutoProxy 类 (注:1)
@Bean
public HttpLogAspect httpLogAspect(){
return new HttpLogAspect();
}
启动容器并访问
header 参数可以通过 curl 验证。(注:2)
测试
可测试的代码才是好代码。所以其实我没写过几行好代码。。。
日志输出到控制台,可以通过获取控制台中内容进行 contains 验证
获取控制台输出字符
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
执行 request 请求
基于 spring test 提供的 mockMvc 方法,测试代码如下:
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Random;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserApiTest {
private MockMvc mockMvc;
@Autowired
private UserApi userApi;
@Autowired
private WebApplicationContext context;
private ByteArrayOutputStream outContent;
private int userId = new Random().nextInt(10);
private int age = new Random().nextInt(10);
private long username = new Random().nextLong();
private long password = new Random().nextLong();
@Before
public void setup() {
// 坚挺控制台输出
outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
//项目拦截器有效
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
//单个类,拦截器无效
// mockMvc = MockMvcBuilders.standaloneSteup(userApi).build();
}
@Test
public void test_log() throws Exception {
String path = "/user/log/" + userId;
String uri = path + "?age=" + age;
RequestBuilder request = MockMvcRequestBuilders.get(uri)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON);
mockMvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk());
String console = outContent.toString();
assertTrue(console.contains("REQUEST_LOG"));
assertFalse(console.contains("HEADER_PARAMS"));
assertTrue(console.contains("RESPONSE_LOG"));
assertTrue(console.contains(path));
assertTrue(console.contains(String.valueOf(age)));
assertFalse(console.contains(String.valueOf(username)));
assertFalse(console.contains(String.valueOf(password)));
}
@Test
public void test_log_header() throws Exception {
String path = "/user/log/pwd/" + userId;
String uri = path + "?age=" + age;
RequestBuilder request = MockMvcRequestBuilders.get(uri)
.header("username", username)
.header("password", password)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON);
mockMvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk());
String console = outContent.toString();
assertTrue(console.contains("REQUEST_LOG"));
assertTrue(console.contains("HEADER_PARAMS"));
assertTrue(console.contains("RESPONSE_LOG"));
assertTrue(console.contains(path));
assertTrue(console.contains(String.valueOf(age)));
assertFalse(console.contains(String.valueOf(username)));
assertTrue(console.contains(String.valueOf(password)));
}
@Test
public void test_log_header_excludeResponse() throws Exception {
String path = "/user/log/pwdExcludeResponse/" + userId;
String uri = path + "?age=" + age;
RequestBuilder request = MockMvcRequestBuilders.get(uri)
.header("username", username)
.header("password", password)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON);
mockMvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk());
String console = outContent.toString();
assertTrue(console.contains("REQUEST_LOG"));
assertTrue(console.contains("HEADER_PARAMS"));
assertFalse(console.contains("RESPONSE_LOG"));
assertTrue(console.contains(path));
assertTrue(console.contains(String.valueOf(age)));
assertTrue(console.contains(String.valueOf(username)));
assertFalse(console.contains(String.valueOf(password)));
}
}
注:1 @EnableAspectJAutoProxy
注:2 curl 增加 header 参数
curl -H 'username:123' -H 'password:345'
升级版
- 增加了耗时统计
- traceId 用于追踪,避免参数日志重复打印
- 增加异常捕获 log
@Aspect
public class HttpLogAspect {
private Logger log = LoggerFactory.getLogger(HttpLogAspect.class);
@Pointcut("@annotation(wang.crick.study.httplog.annotation.HttpLog)")
public void logAnnotation() {
}
private ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();
private ThreadLocal<String> traceIdThreadLocal = new ThreadLocal<>();
private Optional<HttpLog> getLogAnnotation(JoinPoint joinPoint) {
if (joinPoint instanceof MethodInvocationProceedingJoinPoint) {
Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(HttpLog.class)) {
return Optional.of(method.getAnnotation(HttpLog.class));
}
}
}
return Optional.empty();
}
private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}
private String getRequestPath(HttpServletRequest request) {
return (null != request.getServletPath() && request.getServletPath().length() > 0)
? request.getServletPath() : request.getPathInfo();
}
@Before("logAnnotation()")
public void requestLog(JoinPoint joinPoint) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
HttpServletRequest request = getRequest();
traceIdThreadLocal.set(UUID.randomUUID().toString());
startTimeThreadLocal.set(System.currentTimeMillis());
List<String> excludes = Arrays.asList(anno.exclude());
List<Object> params = new ArrayList<>();
StringBuilder logMsg = new StringBuilder();
logMsg.append("REQUEST_LOG. traceId:{}. ")
.append("requestUrl: ")
.append(getRequestPath(request))
.append(" -PARAMS- ");
params.add(traceIdThreadLocal.get());
request.getParameterMap().forEach((k, v) -> {
if (!excludes.contains(k)) {
logMsg.append(k).append(": {}, ");
params.add(v);
}
});
if (anno.headerParams().length > 0) {
logMsg.append(" -HEADER_PARAMS- ");
Arrays.asList(anno.headerParams()).forEach(param -> {
logMsg.append(param).append(": {}, ");
params.add(request.getHeader(param));
});
}
log.info(logMsg.toString(), params.toArray());
});
} catch (Exception ignore) {
log.warn("print request log fail!", ignore);
}
}
@AfterReturning(returning = "restApiResponse", pointcut = "logAnnotation()")
public void response(JoinPoint joinPoint, RestApiResponse restApiResponse) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
if (!anno.ignoreResponse()) {
log.info("RESPONSE_LOG. traceId:{}, result:{}, cost:{}",
traceIdThreadLocal.get(), restApiResponse, System.currentTimeMillis() - startTimeThreadLocal.get());
}
});
} catch (Exception ignore) {
log.warn("print response log fail!", ignore);
}
}
@AfterThrowing(throwing = "e", pointcut = "logAnnotation()")
public void throwing(JoinPoint joinPoint, Exception e) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
if (!anno.ignoreResponse()) {
log.info("ERROR_LOG. traceId:{}, cost:{}",
traceIdThreadLocal.get(), System.currentTimeMillis() - startTimeThreadLocal.get(), e);
}
});
} catch (Exception ignore) {
log.warn("print error log fail!", ignore);
}
}
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于