完善 Access Log 的记录
parent
cc9696938a
commit
6bcad5d53f
|
@ -25,10 +25,12 @@ public class MallUtil {
|
||||||
request.setAttribute(MallConstants.REQUEST_ATTR_USER_TYPE_KEY, userType);
|
request.setAttribute(MallConstants.REQUEST_ATTR_USER_TYPE_KEY, userType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public static CommonResult getCommonResult(ServletRequest request) {
|
public static CommonResult getCommonResult(ServletRequest request) {
|
||||||
return (CommonResult) request.getAttribute(MallConstants.REQUEST_ATTR_COMMON_RESULT);
|
return (CommonResult) request.getAttribute(MallConstants.REQUEST_ATTR_COMMON_RESULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public static void setCommonResult(ServletRequest request, CommonResult result) {
|
public static void setCommonResult(ServletRequest request, CommonResult result) {
|
||||||
request.setAttribute(MallConstants.REQUEST_ATTR_COMMON_RESULT, result);
|
request.setAttribute(MallConstants.REQUEST_ATTR_COMMON_RESULT, result);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,6 @@
|
||||||
<artifactId>mall-spring-boot-starter-swagger</artifactId>
|
<artifactId>mall-spring-boot-starter-swagger</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
|
||||||
<!-- Spring 核心 -->
|
<!-- Spring 核心 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<groupId>cn.iocoder.mall</groupId>
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
<artifactId>system-rpc-api</artifactId>
|
<artifactId>system-rpc-api</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring 核心 -->
|
<!-- Spring 核心 -->
|
||||||
|
|
|
@ -1,23 +1,53 @@
|
||||||
package cn.iocoder.mall.web.config;
|
package cn.iocoder.mall.web.config;
|
||||||
|
|
||||||
|
import cn.iocoder.mall.web.constant.CommonMallConstants;
|
||||||
|
import cn.iocoder.mall.web.handler.GlobalResponseBodyHandler;
|
||||||
import cn.iocoder.mall.web.interceptor.AccessLogInterceptor;
|
import cn.iocoder.mall.web.interceptor.AccessLogInterceptor;
|
||||||
import org.apache.dubbo.config.annotation.Reference;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // TODO 芋艿,未来可能考虑 REACTIVE
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||||
public class CommonWebAutoConfiguration implements WebMvcConfigurer {
|
public class CommonWebAutoConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Reference(validation = "true", version = "${dubbo.consumer.AdminAccessLogService.version:1.0.0}")
|
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
// @Reference(validation = "true", version = "1.0.0")
|
|
||||||
// private SystemLogRPC systemLogRPC;
|
// ========== 全局处理器 ==========
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(GlobalResponseBodyHandler.class)
|
||||||
|
public GlobalResponseBodyHandler globalResponseBodyHandler() {
|
||||||
|
return new GlobalResponseBodyHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 拦截器相关 ==========
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = "cn.iocoder.mall.system.rpc.api.SystemLogRPC")
|
||||||
|
@ConditionalOnMissingBean(AccessLogInterceptor.class)
|
||||||
public AccessLogInterceptor accessLogInterceptor() {
|
public AccessLogInterceptor accessLogInterceptor() {
|
||||||
return new AccessLogInterceptor();
|
return new AccessLogInterceptor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
try {
|
||||||
|
AccessLogInterceptor accessLogInterceptor = this.accessLogInterceptor();
|
||||||
|
if (accessLogInterceptor != null) {
|
||||||
|
registry.addInterceptor(accessLogInterceptor)
|
||||||
|
.addPathPatterns(CommonMallConstants.ROOT_PATH_ADMIN + "/**", CommonMallConstants.ROOT_PATH_USER + "/**");
|
||||||
|
}
|
||||||
|
} catch (NoSuchBeanDefinitionException e) {
|
||||||
|
logger.warn("[addInterceptors][无法获取 AccessLogInterceptor 拦截器,因此不启动 AccessLog 的记录]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package cn.iocoder.mall.web.constant;
|
||||||
|
|
||||||
|
public interface CommonMallConstants {
|
||||||
|
|
||||||
|
// 全局请求路径枚举类,用于定义不同用户类型的根请求路径
|
||||||
|
/**
|
||||||
|
* 根路径 - 用户
|
||||||
|
*/
|
||||||
|
String ROOT_PATH_USER = "/users";
|
||||||
|
/**
|
||||||
|
* 根路径 - 管理员
|
||||||
|
*/
|
||||||
|
String ROOT_PATH_ADMIN = "/admins";
|
||||||
|
|
||||||
|
// HTTP Request Attr
|
||||||
|
/**
|
||||||
|
* HTTP Request Attr - 账号编号
|
||||||
|
*/
|
||||||
|
String REQUEST_ATTR_USER_ID_KEY = "mall_account_id";
|
||||||
|
/**
|
||||||
|
* HTTP Request Attr - Controller 执行返回
|
||||||
|
*/
|
||||||
|
String REQUEST_ATTR_COMMON_RESULT = "mall_common_result";
|
||||||
|
/**
|
||||||
|
* HTTP Request Attr - 访问开始时间
|
||||||
|
*/
|
||||||
|
String REQUEST_ATTR_ACCESS_START_TIME = "mall_access_start_time";
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,21 +1,45 @@
|
||||||
package cn.iocoder.mall.web.handler;
|
package cn.iocoder.mall.web.handler;
|
||||||
|
|
||||||
//@ControllerAdvice
|
import cn.iocoder.common.framework.vo.CommonResult;
|
||||||
//public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
|
import cn.iocoder.mall.web.util.CommonWebUtil;
|
||||||
//
|
import org.springframework.core.MethodParameter;
|
||||||
// @Override
|
import org.springframework.http.MediaType;
|
||||||
// public boolean supports(MethodParameter returnType, Class converterType) {
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
// if (returnType.getMethod() == null) {
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
// return false;
|
import org.springframework.http.server.ServletServerHttpRequest;
|
||||||
// }
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
// return returnType.getMethod().getReturnType() == CommonResult.class;
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
// }
|
|
||||||
//
|
/**
|
||||||
// @Override
|
* 全局响应结果(ResponseBody)处理器
|
||||||
// public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
|
*
|
||||||
// ServerHttpRequest request, ServerHttpResponse response) {
|
* 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult},
|
||||||
// MallUtil.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body);
|
* 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。
|
||||||
// return body;
|
* 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构
|
||||||
// }
|
*
|
||||||
//
|
* 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果,
|
||||||
//}
|
* 方便 {@link cn.iocoder.mall.web.interceptor.AccessLogInterceptor} 记录访问日志
|
||||||
|
*/
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
|
||||||
|
public boolean supports(MethodParameter returnType, Class converterType) {
|
||||||
|
if (returnType.getMethod() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 只拦截返回结果为 CommonResult 类型
|
||||||
|
return returnType.getMethod().getReturnType() == CommonResult.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("NullableProblems") // 避免 IDEA 警告
|
||||||
|
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
|
||||||
|
ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
// 记录 Controller 结果
|
||||||
|
CommonWebUtil.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -1,102 +1,86 @@
|
||||||
package cn.iocoder.mall.web.interceptor;
|
package cn.iocoder.mall.web.interceptor;
|
||||||
|
|
||||||
import org.springframework.beans.BeansException;
|
import cn.iocoder.common.framework.util.HttpUtil;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import cn.iocoder.common.framework.util.MallUtil;
|
||||||
import org.springframework.context.ApplicationContext;
|
import cn.iocoder.common.framework.vo.CommonResult;
|
||||||
import org.springframework.context.ApplicationContextAware;
|
import cn.iocoder.mall.system.rpc.api.SystemLogRPC;
|
||||||
|
import cn.iocoder.mall.system.rpc.request.system.AccessLogAddRequest;
|
||||||
|
import cn.iocoder.mall.web.util.CommonWebUtil;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
import org.apache.dubbo.config.annotation.Reference;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 访问日志拦截器
|
* 访问日志拦截器
|
||||||
*/
|
*/
|
||||||
public class AccessLogInterceptor extends HandlerInterceptorAdapter implements InitializingBean, ApplicationContextAware {
|
public class AccessLogInterceptor extends HandlerInterceptorAdapter {
|
||||||
|
|
||||||
private ApplicationContext context;
|
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
//
|
@Reference(validation = "true", version = "${dubbo.consumer.SystemLogRPC.version}")
|
||||||
// private Logger logger = LoggerFactory.getLogger(getClass());
|
private SystemLogRPC systemLogRPC;
|
||||||
//
|
|
||||||
// /**
|
@Value("${spring.application.name}")
|
||||||
// * 开始时间
|
private String applicationName;
|
||||||
// */
|
|
||||||
// private static final ThreadLocal<Date> START_TIME = new ThreadLocal<>();
|
|
||||||
//
|
|
||||||
// @Reference(validation = "true", version = "${dubbo.consumer.AdminAccessLogService.version:1.0.0}")
|
|
||||||
// private SystemLogService systemAccessLogService;
|
|
||||||
//
|
|
||||||
// @Value("${spring.application.name}")
|
|
||||||
// private String applicationName;
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
|
||||||
// // 记录当前时间
|
|
||||||
// START_TIME.set(new Date());
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
|
||||||
// AccessLogAddDTO accessLog = new AccessLogAddDTO();
|
|
||||||
// try {
|
|
||||||
// // 初始化 accessLog
|
|
||||||
// initAccessLog(accessLog, request);
|
|
||||||
// // 执行插入 accessLog
|
|
||||||
// addAccessLog(accessLog);
|
|
||||||
// // TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
|
|
||||||
// } catch (Throwable th) {
|
|
||||||
// logger.error("[afterCompletion][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
|
||||||
// } finally {
|
|
||||||
// clear();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private void initAccessLog(AccessLogAddDTO accessLog, HttpServletRequest request) {
|
|
||||||
// // 设置用户编号
|
|
||||||
// accessLog.setUserId(MallUtil.getUserId(request));
|
|
||||||
// if (accessLog.getUserId() == null) {
|
|
||||||
// accessLog.setUserId(AccessLogAddDTO.USER_ID_NULL);
|
|
||||||
// }
|
|
||||||
// accessLog.setUserType(MallUtil.getUserType(request));
|
|
||||||
// // 设置访问结果
|
|
||||||
// CommonResult result = MallUtil.getCommonResult(request);
|
|
||||||
// Assert.isTrue(result != null, "result 必须非空");
|
|
||||||
// accessLog.setErrorCode(result.getCode())
|
|
||||||
// .setErrorMessage(result.getMessage());
|
|
||||||
// // 设置其它字段
|
|
||||||
// accessLog.setTraceId(MallUtil.getTraceId())
|
|
||||||
// .setApplicationName(applicationName)
|
|
||||||
// .setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
|
|
||||||
// .setQueryString(HttpUtil.buildQueryString(request))
|
|
||||||
// .setMethod(request.getMethod())
|
|
||||||
// .setUserAgent(HttpUtil.getUserAgent(request))
|
|
||||||
// .setIp(HttpUtil.getIp(request))
|
|
||||||
// .setStartTime(START_TIME.get())
|
|
||||||
// .setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime())); // 默认响应时间设为 0
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Async // 异步入库
|
|
||||||
// public void addAccessLog(AccessLogAddDTO accessLog) {
|
|
||||||
// try {
|
|
||||||
// systemAccessLogService.addAccessLog(accessLog);
|
|
||||||
// } catch (Throwable th) {
|
|
||||||
// logger.error("[addAccessLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private static void clear() {
|
|
||||||
// START_TIME.remove();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||||
this.context = context;
|
// 记录当前时间
|
||||||
|
CommonWebUtil.setAccessStartTime(request, new Date());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() throws Exception {
|
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||||
System.out.println();
|
AccessLogAddRequest accessLog = new AccessLogAddRequest();
|
||||||
|
try {
|
||||||
|
// 初始化 accessLog
|
||||||
|
initAccessLog(accessLog, request);
|
||||||
|
// 执行插入 accessLog
|
||||||
|
addAccessLog(accessLog);
|
||||||
|
// TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
|
||||||
|
} catch (Throwable th) {
|
||||||
|
logger.error("[afterCompletion][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initAccessLog(AccessLogAddRequest accessLog, HttpServletRequest request) {
|
||||||
|
// 设置用户编号
|
||||||
|
accessLog.setAccountId(CommonWebUtil.getAccountId(request));
|
||||||
|
// 设置访问结果
|
||||||
|
CommonResult result = CommonWebUtil.getCommonResult(request);
|
||||||
|
Assert.isTrue(result != null, "result 必须非空");
|
||||||
|
accessLog.setErrorCode(result.getCode())
|
||||||
|
.setErrorMessage(result.getMessage());
|
||||||
|
// 设置其它字段
|
||||||
|
accessLog.setTraceId(MallUtil.getTraceId())
|
||||||
|
.setApplicationName(applicationName)
|
||||||
|
.setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
|
||||||
|
.setQueryString(HttpUtil.buildQueryString(request))
|
||||||
|
.setMethod(request.getMethod())
|
||||||
|
.setUserAgent(HttpUtil.getUserAgent(request))
|
||||||
|
.setIp(HttpUtil.getIp(request))
|
||||||
|
.setStartTime(CommonWebUtil.getAccessStartTime(request))
|
||||||
|
.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime())); // 默认响应时间设为 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async // 异步入库
|
||||||
|
public void addAccessLog(AccessLogAddRequest accessLog) {
|
||||||
|
try {
|
||||||
|
systemLogRPC.addAccessLog(accessLog);
|
||||||
|
} catch (Throwable th) {
|
||||||
|
logger.error("[addAccessLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package cn.iocoder.mall.web.util;
|
||||||
|
|
||||||
|
import cn.iocoder.common.framework.vo.CommonResult;
|
||||||
|
import cn.iocoder.mall.web.constant.CommonMallConstants;
|
||||||
|
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class CommonWebUtil {
|
||||||
|
|
||||||
|
public static Integer getAccountId(ServletRequest request) {
|
||||||
|
return (Integer) request.getAttribute(CommonMallConstants.REQUEST_ATTR_USER_ID_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAccountId(ServletRequest request, Integer userId) {
|
||||||
|
request.setAttribute(CommonMallConstants.REQUEST_ATTR_USER_ID_KEY, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CommonResult getCommonResult(ServletRequest request) {
|
||||||
|
return (CommonResult) request.getAttribute(CommonMallConstants.REQUEST_ATTR_COMMON_RESULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCommonResult(ServletRequest request, CommonResult result) {
|
||||||
|
request.setAttribute(CommonMallConstants.REQUEST_ATTR_COMMON_RESULT, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAccessStartTime(ServletRequest request, Date startTime) {
|
||||||
|
request.setAttribute(CommonMallConstants.REQUEST_ATTR_ACCESS_START_TIME, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Date getAccessStartTime(ServletRequest request) {
|
||||||
|
return (Date) request.getAttribute(CommonMallConstants.REQUEST_ATTR_ACCESS_START_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,58 +0,0 @@
|
||||||
package cn.iocoder.mall.spring.boot.swagger;
|
|
||||||
|
|
||||||
import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
|
||||||
import springfox.documentation.builders.PathSelectors;
|
|
||||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
|
||||||
import springfox.documentation.service.ApiInfo;
|
|
||||||
import springfox.documentation.spi.DocumentationType;
|
|
||||||
import springfox.documentation.spring.web.plugins.Docket;
|
|
||||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单的 Swagger2 自动配置类
|
|
||||||
*
|
|
||||||
* 较为完善的,可以了解 https://mvnrepository.com/artifact/com.spring4all/spring-boot-starter-swagger
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableSwagger2
|
|
||||||
@EnableSwaggerBootstrapUI
|
|
||||||
@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
|
|
||||||
@ConditionalOnProperty(prefix = "swagger", value = "enable", matchIfMissing = true) // 允许使用 swagger.enable=false 禁用 Swagger
|
|
||||||
@EnableConfigurationProperties(SwaggerProperties.class)
|
|
||||||
@Deprecated
|
|
||||||
public class SwaggerAutoConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
public SwaggerProperties swaggerProperties() {
|
|
||||||
return new SwaggerProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Docket createRestApi() {
|
|
||||||
SwaggerProperties properties = swaggerProperties();
|
|
||||||
// 创建 Docket 对象
|
|
||||||
return new Docket(DocumentationType.SWAGGER_2)
|
|
||||||
.apiInfo(apiInfo(properties))
|
|
||||||
.select()
|
|
||||||
.apis(RequestHandlerSelectors.basePackage(properties.getBasePackage()))
|
|
||||||
.paths(PathSelectors.any())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ApiInfo apiInfo(SwaggerProperties properties) {
|
|
||||||
return new ApiInfoBuilder()
|
|
||||||
.title(properties.getTitle())
|
|
||||||
.description(properties.getDescription())
|
|
||||||
.version(properties.getVersion())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package cn.iocoder.mall.spring.boot.swagger;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@ConfigurationProperties("swagger")
|
|
||||||
@Deprecated
|
|
||||||
public class SwaggerProperties {
|
|
||||||
|
|
||||||
private String title;
|
|
||||||
private String description;
|
|
||||||
private String version;
|
|
||||||
private String basePackage;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package cn.iocoder.mall.spring.boot.web.handler;
|
|
||||||
|
|
||||||
import cn.iocoder.common.framework.util.MallUtil;
|
|
||||||
import cn.iocoder.common.framework.vo.CommonResult;
|
|
||||||
import org.springframework.core.MethodParameter;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
|
||||||
import org.springframework.http.server.ServletServerHttpRequest;
|
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
|
||||||
|
|
||||||
@ControllerAdvice
|
|
||||||
public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supports(MethodParameter returnType, Class converterType) {
|
|
||||||
if (returnType.getMethod() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return returnType.getMethod().getReturnType() == CommonResult.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
|
|
||||||
ServerHttpRequest request, ServerHttpResponse response) {
|
|
||||||
MallUtil.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body);
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package cn.iocoder.mall.spring.boot.web.interceptor;
|
|
||||||
|
|
||||||
import cn.iocoder.common.framework.util.HttpUtil;
|
|
||||||
import cn.iocoder.common.framework.util.MallUtil;
|
|
||||||
import cn.iocoder.common.framework.vo.CommonResult;
|
|
||||||
import cn.iocoder.mall.system.api.SystemLogService;
|
|
||||||
import cn.iocoder.mall.system.api.dto.systemlog.AccessLogAddDTO;
|
|
||||||
import com.alibaba.fastjson.JSON;
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
|
||||||
import org.apache.dubbo.config.annotation.Reference;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问日志拦截器
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class AccessLogInterceptor extends HandlerInterceptorAdapter {
|
|
||||||
|
|
||||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始时间
|
|
||||||
*/
|
|
||||||
private static final ThreadLocal<Date> START_TIME = new ThreadLocal<>();
|
|
||||||
|
|
||||||
@Reference(validation = "true", version = "${dubbo.consumer.AdminAccessLogService.version:1.0.0}")
|
|
||||||
private SystemLogService systemAccessLogService;
|
|
||||||
|
|
||||||
@Value("${spring.application.name}")
|
|
||||||
private String applicationName;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
|
||||||
// 记录当前时间
|
|
||||||
START_TIME.set(new Date());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
|
||||||
AccessLogAddDTO accessLog = new AccessLogAddDTO();
|
|
||||||
try {
|
|
||||||
// 初始化 accessLog
|
|
||||||
initAccessLog(accessLog, request);
|
|
||||||
// 执行插入 accessLog
|
|
||||||
addAccessLog(accessLog);
|
|
||||||
// TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
|
|
||||||
} catch (Throwable th) {
|
|
||||||
logger.error("[afterCompletion][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
|
||||||
} finally {
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initAccessLog(AccessLogAddDTO accessLog, HttpServletRequest request) {
|
|
||||||
// 设置用户编号
|
|
||||||
accessLog.setUserId(MallUtil.getUserId(request));
|
|
||||||
if (accessLog.getUserId() == null) {
|
|
||||||
accessLog.setUserId(AccessLogAddDTO.USER_ID_NULL);
|
|
||||||
}
|
|
||||||
accessLog.setUserType(MallUtil.getUserType(request));
|
|
||||||
// 设置访问结果
|
|
||||||
CommonResult result = MallUtil.getCommonResult(request);
|
|
||||||
Assert.isTrue(result != null, "result 必须非空");
|
|
||||||
accessLog.setErrorCode(result.getCode())
|
|
||||||
.setErrorMessage(result.getMessage());
|
|
||||||
// 设置其它字段
|
|
||||||
accessLog.setTraceId(MallUtil.getTraceId())
|
|
||||||
.setApplicationName(applicationName)
|
|
||||||
.setUri(request.getRequestURI()) // TODO 提升:如果想要优化,可以使用 Swagger 的 @ApiOperation 注解。
|
|
||||||
.setQueryString(HttpUtil.buildQueryString(request))
|
|
||||||
.setMethod(request.getMethod())
|
|
||||||
.setUserAgent(HttpUtil.getUserAgent(request))
|
|
||||||
.setIp(HttpUtil.getIp(request))
|
|
||||||
.setStartTime(START_TIME.get())
|
|
||||||
.setResponseTime((int) (System.currentTimeMillis() - accessLog.getStartTime().getTime())); // 默认响应时间设为 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Async // 异步入库
|
|
||||||
public void addAccessLog(AccessLogAddDTO accessLog) {
|
|
||||||
try {
|
|
||||||
systemAccessLogService.addAccessLog(accessLog);
|
|
||||||
} catch (Throwable th) {
|
|
||||||
logger.error("[addAccessLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void clear() {
|
|
||||||
START_TIME.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||||
cn.iocoder.mall.spring.boot.web.AdminMVCAutoConfiguration, \
|
cn.iocoder.mall.spring.boot.web.AdminMVCAutoConfiguration, \
|
||||||
cn.iocoder.mall.spring.boot.web.UserMVCAutoConfiguration, \
|
cn.iocoder.mall.spring.boot.web.UserMVCAutoConfiguration, \
|
||||||
cn.iocoder.mall.spring.boot.swagger.SwaggerAutoConfiguration, \
|
|
||||||
cn.iocoder.mall.spring.boot.metrics.MetricsAutoConfiguration
|
cn.iocoder.mall.spring.boot.metrics.MetricsAutoConfiguration
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
spring:
|
spring:
|
||||||
# Application 的配置项
|
# Application 的配置项
|
||||||
application:
|
application:
|
||||||
name: admin-application
|
name: system-application
|
||||||
# Profile 的配置项
|
# Profile 的配置项
|
||||||
profiles:
|
profiles:
|
||||||
active: local
|
active: local
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
package cn.iocoder.mall.system.biz.dto.system;
|
package cn.iocoder.mall.system.biz.dto.system;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 访问日志添加 DTO
|
* 访问日志添加 DTO
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
public class AccessLogAddDTO {
|
public class AccessLogAddDTO {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,11 +19,15 @@
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 工具类 -->
|
<!-- 工具类相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.validation</groupId>
|
<groupId>javax.validation</groupId>
|
||||||
<artifactId>validation-api</artifactId>
|
<artifactId>validation-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
package cn.iocoder.mall.system.rpc.request.system;
|
package cn.iocoder.mall.system.rpc.request.system;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问日志添加请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
public class AccessLogAddRequest {
|
public class AccessLogAddRequest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,7 +18,9 @@ public class AccessLogAddRequest {
|
||||||
*/
|
*/
|
||||||
public static final Integer ACCOUNT_ID_NULL = 0;
|
public static final Integer ACCOUNT_ID_NULL = 0;
|
||||||
|
|
||||||
@NotNull(message = "链路追踪编号不能为空")
|
/**
|
||||||
|
* 链路追踪编号
|
||||||
|
*/
|
||||||
private String traceId;
|
private String traceId;
|
||||||
/**
|
/**
|
||||||
* 账号编号
|
* 账号编号
|
||||||
|
|
|
@ -9,7 +9,7 @@ import cn.iocoder.mall.system.rpc.request.system.AccessLogAddRequest;
|
||||||
import org.apache.dubbo.config.annotation.Service;
|
import org.apache.dubbo.config.annotation.Service;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
@Service(version = "dubbo.provider.SystemLogRPC.version", validation = "true")
|
@Service(version = "${dubbo.provider.SystemLogRPC.version}", validation = "true")
|
||||||
public class SystemLogRPCImpl implements SystemLogRPC {
|
public class SystemLogRPCImpl implements SystemLogRPC {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|
|
@ -15,3 +15,7 @@ dubbo:
|
||||||
filter: -exception
|
filter: -exception
|
||||||
SystemLogRPC:
|
SystemLogRPC:
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
# Dubbo 服务消费者的配置
|
||||||
|
consumer:
|
||||||
|
SystemLogRPC: # 用于 AccessLogInterceptor 等拦截器,记录 HTTP API 请求的访问日志
|
||||||
|
version: 1.0.0
|
||||||
|
|
|
@ -15,10 +15,17 @@
|
||||||
<!-- Mall 相关 -->
|
<!-- Mall 相关 -->
|
||||||
<!-- <dependency>-->
|
<!-- <dependency>-->
|
||||||
<!-- <groupId>cn.iocoder.mall</groupId>-->
|
<!-- <groupId>cn.iocoder.mall</groupId>-->
|
||||||
<!-- <artifactId>system-biz</artifactId>-->
|
<!-- <artifactId>user-biz</artifactId>-->
|
||||||
<!-- <version>1.0-SNAPSHOT</version>-->
|
<!-- <version>1.0-SNAPSHOT</version>-->
|
||||||
<!-- </dependency>-->
|
<!-- </dependency>-->
|
||||||
|
|
||||||
|
<!-- Mall 相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
|
<artifactId>common-framework</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.mall</groupId>
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package cn.iocoder.mall.user.rest.controller;
|
||||||
|
|
||||||
|
import cn.iocoder.common.framework.vo.CommonResult;
|
||||||
|
import cn.iocoder.mall.web.constant.CommonMallConstants;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(CommonMallConstants.ROOT_PATH_USER + "/test")
|
||||||
|
public class TestController {
|
||||||
|
|
||||||
|
@GetMapping("/echo")
|
||||||
|
public CommonResult<Boolean> echo() {
|
||||||
|
return CommonResult.success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package cn.iocoder.mall.user.rest.controller;
|
|
@ -0,0 +1 @@
|
||||||
|
package cn.iocoder.mall.user.rest;
|
|
@ -13,11 +13,11 @@
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Mall 相关 -->
|
<!-- Mall 相关 -->
|
||||||
<dependency>
|
<!-- <dependency>-->
|
||||||
<groupId>cn.iocoder.mall</groupId>
|
<!-- <groupId>cn.iocoder.mall</groupId>-->
|
||||||
<artifactId>system-rpc-api</artifactId>
|
<!-- <artifactId>system-rpc-api</artifactId>-->
|
||||||
<version>1.0-SNAPSHOT</version>
|
<!-- <version>1.0-SNAPSHOT</version>-->
|
||||||
</dependency>
|
<!-- </dependency>-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.mall</groupId>
|
<groupId>cn.iocoder.mall</groupId>
|
||||||
<artifactId>user-rpc-api</artifactId>
|
<artifactId>user-rpc-api</artifactId>
|
||||||
|
|
Loading…
Reference in New Issue