- 后端:替换 swagger bootstrap ui ,一下子接口文档好看了。

- 后端:增加 swagger AutoConfiguration 配置类
- 后端:统一访问日志的记录
This commit is contained in:
YunaiV
2019-05-10 18:47:29 +08:00
parent 3ff9f1b326
commit 53fff39a6c
67 changed files with 899 additions and 894 deletions

View File

@@ -1,17 +0,0 @@
package cn.iocoder.mall.spring.boot.constant;
/**
* 全局请求路径枚举类,用于定义不同用户类型的根请求路径
*/
public interface RootRequestPath {
/**
* 管理员
*/
String ADMIN = "/admins";
/**
* 用户
*/
String USER = "/users";
}

View File

@@ -0,0 +1,57 @@
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)
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();
}
}

View File

@@ -0,0 +1,15 @@
package cn.iocoder.mall.spring.boot.swagger;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {
private String title;
private String description;
private String version;
private String basePackage;
}

View File

@@ -0,0 +1,64 @@
package cn.iocoder.mall.spring.boot.web;
import cn.iocoder.common.framework.constant.MallConstants;
import cn.iocoder.common.framework.servlet.CorsFilter;
import cn.iocoder.mall.spring.boot.web.interceptor.AccessLogInterceptor;
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
import cn.iocoder.mall.spring.boot.web.handler.GlobalExceptionHandler;
import cn.iocoder.mall.spring.boot.web.handler.GlobalResponseBodyHandler;
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.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // TODO 芋艿,未来可能考虑 REACTIVE
@ConditionalOnClass({DispatcherServlet.class, WebMvcConfigurer.class, // 有 Spring MVC 容器
AdminSecurityInterceptor.class, AccessLogInterceptor.class}) // 有引入 system-sdk
public class AdminMVCAutoConfiguration implements WebMvcConfigurer {
@Bean
// @ConditionalOnMissingBean(AccessLogInterceptor.class)
public AccessLogInterceptor adminAccessLogInterceptor() {
return new AccessLogInterceptor();
}
@Bean
@ConditionalOnMissingBean(AdminSecurityInterceptor.class)
public AdminSecurityInterceptor adminSecurityInterceptor() {
return new AdminSecurityInterceptor();
}
@Bean
@ConditionalOnMissingBean(GlobalResponseBodyHandler.class)
public GlobalResponseBodyHandler globalReturnValueHandler() {
return new GlobalResponseBodyHandler();
}
@Bean
@ConditionalOnMissingBean(GlobalResponseBodyHandler.class)
public GlobalExceptionHandler globalExceptionHandler() {
return new GlobalExceptionHandler();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminAccessLogInterceptor()).addPathPatterns(MallConstants.ROOT_PATH_ADMIN + "/**");
registry.addInterceptor(adminSecurityInterceptor()).addPathPatterns(MallConstants.ROOT_PATH_ADMIN + "/**");
}
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBean<CorsFilter> corsFilter() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CorsFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}

View File

@@ -1,49 +0,0 @@
package cn.iocoder.mall.spring.boot.web;
import cn.iocoder.mall.admin.sdk.interceptor.AdminAccessLogInterceptor;
import cn.iocoder.mall.admin.sdk.interceptor.AdminSecurityInterceptor;
import cn.iocoder.mall.spring.boot.constant.RootRequestPath;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // TODO 芋艿,未来可能考虑 REACTIVE
@ConditionalOnClass({DispatcherServlet.class, WebMvcConfigurer.class, // 有 Spring MVC 容器
AdminSecurityInterceptor.class, AdminAccessLogInterceptor.class}) // 有引入 system-sdk
public class AdminMVCConfiguration implements WebMvcConfigurer {
@Bean
@ConditionalOnMissingBean(AdminSecurityInterceptor.class)
public AdminSecurityInterceptor adminSecurityInterceptor() {
return new AdminSecurityInterceptor();
}
@Bean
@ConditionalOnMissingBean(AdminAccessLogInterceptor.class)
public AdminAccessLogInterceptor adminAccessLogInterceptor() {
return new AdminAccessLogInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminAccessLogInterceptor()).addPathPatterns(RootRequestPath.ADMIN + "/**");
registry.addInterceptor(adminSecurityInterceptor()).addPathPatterns(RootRequestPath.ADMIN + "/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping(RootRequestPath.USER + "/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true).maxAge(1800);
}
}

View File

@@ -0,0 +1,65 @@
package cn.iocoder.mall.spring.boot.web;
import cn.iocoder.common.framework.constant.MallConstants;
import cn.iocoder.common.framework.servlet.CorsFilter;
import cn.iocoder.mall.spring.boot.web.interceptor.AccessLogInterceptor;
import cn.iocoder.mall.spring.boot.web.handler.GlobalExceptionHandler;
import cn.iocoder.mall.spring.boot.web.handler.GlobalResponseBodyHandler;
import cn.iocoder.mall.user.sdk.interceptor.UserSecurityInterceptor;
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.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // TODO 芋艿,未来可能考虑 REACTIVE
@ConditionalOnClass({DispatcherServlet.class, WebMvcConfigurer.class, // 有 Spring MVC 容器
UserSecurityInterceptor.class, // 有引入 user-sdk
AccessLogInterceptor.class}) // 有引入 system-sdk
public class UserMVCAutoConfiguration implements WebMvcConfigurer {
@Bean
// @ConditionalOnMissingBean(AccessLogInterceptor.class)
public AccessLogInterceptor userAccessLogInterceptor() {
return new AccessLogInterceptor();
}
@Bean
@ConditionalOnMissingBean(UserSecurityInterceptor.class)
public UserSecurityInterceptor userSecurityInterceptor() {
return new UserSecurityInterceptor();
}
@Bean
@ConditionalOnMissingBean(GlobalResponseBodyHandler.class)
public GlobalResponseBodyHandler globalReturnValueHandler() {
return new GlobalResponseBodyHandler();
}
@Bean
@ConditionalOnMissingBean(GlobalExceptionHandler.class)
public GlobalExceptionHandler globalExceptionHandler() {
return new GlobalExceptionHandler();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userAccessLogInterceptor()).addPathPatterns(MallConstants.ROOT_PATH_USER + "/**");
registry.addInterceptor(userSecurityInterceptor()).addPathPatterns(MallConstants.ROOT_PATH_USER + "/**");
}
@Bean
@ConditionalOnMissingBean
public FilterRegistrationBean<CorsFilter> corsFilter() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CorsFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}

View File

@@ -1,49 +0,0 @@
package cn.iocoder.mall.spring.boot.web;
import cn.iocoder.mall.spring.boot.constant.RootRequestPath;
import cn.iocoder.mall.user.sdk.interceptor.UserAccessLogInterceptor;
import cn.iocoder.mall.user.sdk.interceptor.UserSecurityInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // TODO 芋艿,未来可能考虑 REACTIVE
@ConditionalOnClass({DispatcherServlet.class, WebMvcConfigurer.class, // 有 Spring MVC 容器
UserSecurityInterceptor.class, UserAccessLogInterceptor.class}) // 有引入 system-sdk
public class UserMVCConfiguration implements WebMvcConfigurer {
@Bean
@ConditionalOnMissingBean(UserAccessLogInterceptor.class)
public UserAccessLogInterceptor userAccessLogInterceptor() {
return new UserAccessLogInterceptor();
}
@Bean
@ConditionalOnMissingBean(UserSecurityInterceptor.class)
public UserSecurityInterceptor userSecurityInterceptor() {
return new UserSecurityInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userAccessLogInterceptor()).addPathPatterns(RootRequestPath.USER + "/**");
registry.addInterceptor(userSecurityInterceptor()).addPathPatterns(RootRequestPath.USER + "/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping(RootRequestPath.USER + "/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true).maxAge(1800);
}
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.mall.spring.boot.web.handler;
import cn.iocoder.common.framework.constant.SysErrorCodeEnum;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.vo.CommonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
// 逻辑异常
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
public CommonResult serviceExceptionHandler(HttpServletRequest req, ServiceException ex) {
logger.debug("[serviceExceptionHandler]", ex);
return CommonResult.error(ex.getCode(), ex.getMessage());
}
// Spring MVC 参数不正确
@ResponseBody
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {
logger.warn("[missingServletRequestParameterExceptionHandler]", ex);
return CommonResult.error(SysErrorCodeEnum.MISSING_REQUEST_PARAM_ERROR.getCode(), SysErrorCodeEnum.MISSING_REQUEST_PARAM_ERROR.getMessage() + ":" + ex.getMessage());
}
@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
logger.info("[constraintViolationExceptionHandler]", ex);
// TODO 芋艿,后续要想一个更好的方式。
// 拼接详细报错
StringBuilder detailMessage = new StringBuilder("\n\n详细错误如下");
ex.getConstraintViolations().forEach(constraintViolation -> detailMessage.append("\n").append(constraintViolation.getMessage()));
return CommonResult.error(SysErrorCodeEnum.VALIDATION_REQUEST_PARAM_ERROR.getCode(), SysErrorCodeEnum.VALIDATION_REQUEST_PARAM_ERROR.getMessage()
+ detailMessage.toString());
}
@ResponseBody
@ExceptionHandler(value = Exception.class)
public CommonResult resultExceptionHandler(HttpServletRequest req, Exception e) {
logger.error("[resultExceptionHandler]", e);
// 返回
try {
addExceptionLog();
} catch (Throwable th) {
// TODO
}
return CommonResult.error(SysErrorCodeEnum.SYS_ERROR.getCode(), SysErrorCodeEnum.SYS_ERROR.getMessage());
}
// TODO 芋艿,应该还有其它的异常,需要进行翻译
@Async
public void addExceptionLog() {
}
}

View File

@@ -0,0 +1,31 @@
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().isAssignableFrom(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;
}
}

View File

@@ -0,0 +1,97 @@
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.admin.api.SystemLogService;
import cn.iocoder.mall.admin.api.dto.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 adminAccessLogService;
@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.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
// 执行插入
addAccessLog(accessLog);
// TODO 提升:暂时不考虑 ELK 的方案。而是基于 MySQL 存储。如果访问日志比较多,需要定期归档。
} catch (Throwable th) {
logger.error("[afterCompletion][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
} finally {
clear();
}
}
@Async // 异步入库
public void addAccessLog(AccessLogAddDTO accessLog) {
try {
adminAccessLogService.addAccessLog(accessLog);
} catch (Throwable th) {
logger.error("[addAccessLog][插入访问日志({}) 发生异常({})", JSON.toJSONString(accessLog), ExceptionUtils.getRootCauseMessage(th));
}
}
private static void clear() {
START_TIME.remove();
}
}

View File

@@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.mall.spring.boot.web.AdminMVCConfiguration, \
cn.iocoder.mall.spring.boot.web.UserMVCConfiguration
cn.iocoder.mall.spring.boot.web.AdminMVCAutoConfiguration, \
cn.iocoder.mall.spring.boot.web.UserMVCAutoConfiguration, \
cn.iocoder.mall.spring.boot.swagger.SwaggerAutoConfiguration