我正在用spring boot开发REST API。我需要记录所有的请求与输入参数(与方法,例如。GET, POST等),请求路径,查询字符串,此请求对应的类方法,以及此操作的响应,包括成功和错误。例如:
成功的要求:
http://example.com/api/users/1
Log应该是这样的:
{
HttpStatus: 200,
path: "api/users/1",
method: "GET",
clientIp: "0.0.0.0",
accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
method: "UsersController.getUser",
arguments: {
id: 1
},
response: {
user: {
id: 1,
username: "user123",
email: "user123@example.com"
}
},
exceptions: []
}
或请求错误:
http://example.com/api/users/9999
Log应该是这样的:
{
HttpStatus: 404,
errorCode: 101,
path: "api/users/9999",
method: "GET",
clientIp: "0.0.0.0",
accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
method: "UsersController.getUser",
arguments: {
id: 9999
},
returns: {
},
exceptions: [
{
exception: "UserNotFoundException",
message: "User with id 9999 not found",
exceptionId: "adhaskldjaso98d7324kjh989",
stacktrace: ...................
]
}
我希望Request/Response是一个单独的实体,在成功和错误的情况下都具有与该实体相关的自定义信息。
春季实现这一目标的最佳做法是什么,可能是使用过滤器吗?如果是,能否提供具体的例子?
我使用过@ControllerAdvice和@ExceptionHandler,但正如我提到的,我需要在一个地方(和单个日志)处理所有成功和错误请求。
如果您不介意尝试Spring AOP,这是我一直在探索的日志目的,它对我来说工作得很好。它不会记录未定义的请求和失败的请求尝试。
添加这三个依赖项
spring-aop, aspectjrt, aspectjweaver
将此添加到xml配置文件<aop:aspectj-autoproxy/>
创建一个可以用作切入点的注释
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface EnableLogging {
ActionType actionType();
}
现在注释你想要记录的所有API方法
@EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
@Override
public Response getEmployees(RequestDto req, final String param) {
...
}
现在来看方面。组件—扫描这个类所在的包。
@Aspect
@Component
public class Aspects {
@AfterReturning(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
if (result instanceof Response) {
Response responseObj = (Response) result;
String requestUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
+ "?" + request.getQueryString();
String clientIp = request.getRemoteAddr();
String clientRequest = reqArg.toString();
int httpResponseStatus = responseObj.getStatus();
responseObj.getEntity();
// Can log whatever stuff from here in a single spot.
}
@AfterThrowing(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
String requestUrl = request.getScheme() + "://" + request.getServerName()
+ ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
+ "?" + request.getQueryString();
exception.getMessage();
exception.getCause();
exception.printStackTrace();
exception.getLocalizedMessage();
// Can log whatever exceptions, requests, etc from here in a single spot.
}
}
@AfterReturning建议在匹配的方法执行返回时运行
正常。
@ afterthrows通知在匹配的方法执行由退出时运行
抛出异常。
如果你想详细阅读,请通读这个。
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
下面是我如何在春季数据休息
通过使用
org.springframework.web.util.ContentCachingRequestWrapper和
org.springframework.web.util.ContentCachingResponseWrapper
/**
* Doogies very cool HTTP request logging
*
* There is also {@link org.springframework.web.filter.CommonsRequestLoggingFilter} but it cannot log request method
* And it cannot easily be extended.
*
* https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
* http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
*/
public class DoogiesRequestLogger extends OncePerRequestFilter {
private boolean includeResponsePayload = true;
private int maxPayloadLength = 1000;
private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
if (buf == null || buf.length == 0) return "";
int length = Math.min(buf.length, this.maxPayloadLength);
try {
return new String(buf, 0, length, charsetName);
} catch (UnsupportedEncodingException ex) {
return "Unsupported Encoding";
}
}
/**
* Log each request and respponse with full Request URI, content payload and duration of the request in ms.
* @param request the request
* @param response the response
* @param filterChain chain of filters
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
StringBuffer reqInfo = new StringBuffer()
.append("[")
.append(startTime % 10000) // request ID
.append("] ")
.append(request.getMethod())
.append(" ")
.append(request.getRequestURL());
String queryString = request.getQueryString();
if (queryString != null) {
reqInfo.append("?").append(queryString);
}
if (request.getAuthType() != null) {
reqInfo.append(", authType=")
.append(request.getAuthType());
}
if (request.getUserPrincipal() != null) {
reqInfo.append(", principalName=")
.append(request.getUserPrincipal().getName());
}
this.logger.debug("=> " + reqInfo);
// ========= Log request and response payload ("body") ========
// We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
// String reqBody = DoogiesUtil._stream2String(request.getInputStream()); // THIS WOULD NOT WORK!
// So we need to apply some stronger magic here :-)
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse); // ======== This performs the actual request!
long duration = System.currentTimeMillis() - startTime;
// I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
if (requestBody.length() > 0) {
this.logger.debug(" Request body:\n" +requestBody);
}
this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
if (includeResponsePayload) {
byte[] buf = wrappedResponse.getContentAsByteArray();
this.logger.debug(" Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
}
wrappedResponse.copyBodyToResponse(); // IMPORTANT: copy content of response back into original response
}
}