init version
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4" />
|
||||
72
joju-framework/joju-spring-boot-starter-web/pom.xml
Normal file
72
joju-framework/joju-spring-boot-starter-web/pom.xml
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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">
|
||||
<parent>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>joju-spring-boot-starter-web</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>Web 框架,全局异常、API 日志等</description>
|
||||
<url>https://www.jojubanking.com</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- spring boot 配置所需依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<!-- 业务组件 -->
|
||||
<dependency>
|
||||
<groupId>com.jojubanking.boot</groupId>
|
||||
<artifactId>joju-module-infra-api</artifactId> <!-- 需要使用它,进行操作日志的记录 -->
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 服务保障相关 -->
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-ratelimiter</artifactId>
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.jojubanking.boot.framework.apilog.config;
|
||||
|
||||
import com.jojubanking.boot.framework.apilog.core.filter.ApiAccessLogFilter;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl;
|
||||
import com.jojubanking.boot.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.jojubanking.boot.framework.web.config.WebProperties;
|
||||
import com.jojubanking.boot.framework.web.config.JojuWebAutoConfiguration;
|
||||
import com.jojubanking.boot.module.infra.api.logger.ApiAccessLogApi;
|
||||
import com.jojubanking.boot.module.infra.api.logger.ApiErrorLogApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@Configuration
|
||||
@AutoConfigureAfter(JojuWebAutoConfiguration.class)
|
||||
public class JojuApiLogAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) {
|
||||
return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) {
|
||||
return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ApiAccessLogFilter Bean,记录 API 请求日志
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "joju.access-log", value = "enable", matchIfMissing = true)
|
||||
// 允许使用 joju.access-log.enable=false 禁用访问日志
|
||||
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
|
||||
@Value("${spring.application.name}") String applicationName,
|
||||
ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
|
||||
return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER);
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.filter;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiAccessLog;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.common.util.date.DateUtils;
|
||||
import com.jojubanking.boot.framework.common.util.monitor.TracerUtils;
|
||||
import com.jojubanking.boot.framework.common.util.servlet.ServletUtils;
|
||||
import com.jojubanking.boot.framework.web.config.WebProperties;
|
||||
import com.jojubanking.boot.framework.web.core.filter.ApiRequestFilter;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* API 访问日志 Filter
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiAccessLogFilter extends ApiRequestFilter {
|
||||
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
|
||||
|
||||
public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
super(webProperties);
|
||||
this.applicationName = applicationName;
|
||||
this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 获得开始时间
|
||||
Date beginTim = new Date();
|
||||
// 提前获得参数,避免 XssFilter 过滤处理
|
||||
Map<String, String> queryString = ServletUtil.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null;
|
||||
|
||||
try {
|
||||
// 继续过滤器
|
||||
filterChain.doFilter(request, response);
|
||||
// 正常执行,记录日志
|
||||
createApiAccessLog(request, beginTim, queryString, requestBody, null);
|
||||
} catch (Exception ex) {
|
||||
// 异常执行,记录日志
|
||||
createApiAccessLog(request, beginTim, queryString, requestBody, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void createApiAccessLog(HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
ApiAccessLog accessLog = new ApiAccessLog();
|
||||
try {
|
||||
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
|
||||
apiAccessLogFrameworkService.createApiAccessLog(accessLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
// 处理用户信息
|
||||
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
|
||||
// 设置访问结果
|
||||
CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
|
||||
if (result != null) {
|
||||
accessLog.setResultCode(result.getCode());
|
||||
accessLog.setResultMsg(result.getMsg());
|
||||
} else if (ex != null) {
|
||||
accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode());
|
||||
accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
|
||||
} else {
|
||||
accessLog.setResultCode(0);
|
||||
accessLog.setResultMsg("");
|
||||
}
|
||||
// 设置其它字段
|
||||
accessLog.setTraceId(TracerUtils.getTraceId());
|
||||
accessLog.setApplicationName(applicationName);
|
||||
accessLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build();
|
||||
accessLog.setRequestParams(toJsonString(requestParams));
|
||||
accessLog.setRequestMethod(request.getMethod());
|
||||
accessLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
accessLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
// 持续时间
|
||||
accessLog.setBeginTime(beginTime);
|
||||
accessLog.setEndTime(new Date());
|
||||
accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 访问日志
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLog {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 开始请求时间
|
||||
*/
|
||||
@NotNull(message = "开始请求时间不能为空")
|
||||
private Date beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
@NotNull(message = "结束请求时间不能为空")
|
||||
private Date endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
@NotNull(message = "执行时长不能为空")
|
||||
private Integer duration;
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
@NotNull(message = "错误码不能为空")
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.service;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 接口
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public interface ApiAccessLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param apiAccessLog API 访问日志
|
||||
*/
|
||||
void createApiAccessLog(ApiAccessLog apiAccessLog);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.jojubanking.boot.module.infra.api.logger.ApiAccessLogApi;
|
||||
import com.jojubanking.boot.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link ApiAccessLogApi} 服务,记录访问日志
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
|
||||
|
||||
private final ApiAccessLogApi apiAccessLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createApiAccessLog(ApiAccessLog apiAccessLog) {
|
||||
ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class);
|
||||
apiAccessLogApi.createApiAccessLog(reqDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.service;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 错误日志
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Data
|
||||
public class ApiErrorLog {
|
||||
|
||||
/**
|
||||
* 链路编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 账号编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 异常时间
|
||||
*/
|
||||
@NotNull(message = "异常时间不能为空")
|
||||
private Date exceptionTime;
|
||||
/**
|
||||
* 异常名
|
||||
*/
|
||||
@NotNull(message = "异常名不能为空")
|
||||
private String exceptionName;
|
||||
/**
|
||||
* 异常发生的类全名
|
||||
*/
|
||||
@NotNull(message = "异常发生的类全名不能为空")
|
||||
private String exceptionClassName;
|
||||
/**
|
||||
* 异常发生的类文件
|
||||
*/
|
||||
@NotNull(message = "异常发生的类文件不能为空")
|
||||
private String exceptionFileName;
|
||||
/**
|
||||
* 异常发生的方法名
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法名不能为空")
|
||||
private String exceptionMethodName;
|
||||
/**
|
||||
* 异常发生的方法所在行
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法所在行不能为空")
|
||||
private Integer exceptionLineNumber;
|
||||
/**
|
||||
* 异常的栈轨迹异常的栈轨迹
|
||||
*/
|
||||
@NotNull(message = "异常的栈轨迹不能为空")
|
||||
private String exceptionStackTrace;
|
||||
/**
|
||||
* 异常导致的根消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的根消息不能为空")
|
||||
private String exceptionRootCauseMessage;
|
||||
/**
|
||||
* 异常导致的消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.service;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 接口
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public interface ApiErrorLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param apiErrorLog API 错误日志
|
||||
*/
|
||||
void createApiErrorLog(ApiErrorLog apiErrorLog);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jojubanking.boot.framework.apilog.core.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.jojubanking.boot.module.infra.api.logger.ApiErrorLogApi;
|
||||
import com.jojubanking.boot.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 实现类
|
||||
*
|
||||
* 基于 {@link ApiErrorLogApi} 服务,记录错误日志
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService {
|
||||
|
||||
private final ApiErrorLogApi apiErrorLogApi;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void createApiErrorLog(ApiErrorLog apiErrorLog) {
|
||||
ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class);
|
||||
apiErrorLogApi.createApiErrorLog(reqDTO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* API 日志:包含两类
|
||||
* 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
|
||||
* 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
package com.jojubanking.boot.framework.apilog;
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.jojubanking.boot.framework.jackson.config;
|
||||
|
||||
import com.jojubanking.boot.framework.jackson.core.databind.LocalDateTimeDeserializer;
|
||||
import com.jojubanking.boot.framework.jackson.core.databind.LocalDateTimeSerializer;
|
||||
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class JojuJacksonAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public BeanPostProcessor objectMapperBeanPostProcessor() {
|
||||
return new BeanPostProcessor() {
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (!(bean instanceof ObjectMapper)) {
|
||||
return bean;
|
||||
}
|
||||
ObjectMapper objectMapper = (ObjectMapper) bean;
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
/*
|
||||
* 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型
|
||||
* 2. 新增LocalDateTime序列化、反序列化规则
|
||||
*/
|
||||
simpleModule
|
||||
// .addSerializer(Long.class, ToStringSerializer.instance)
|
||||
// .addSerializer(Long.TYPE, ToStringSerializer.instance)
|
||||
.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
|
||||
|
||||
objectMapper.registerModules(simpleModule);
|
||||
|
||||
JsonUtils.init(objectMapper);
|
||||
log.info("初始化 jackson 自动配置");
|
||||
return bean;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.jojubanking.boot.framework.jackson.core.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* LocalDateTime反序列化规则
|
||||
* <p>
|
||||
* 会将毫秒级时间戳反序列化为LocalDateTime
|
||||
*/
|
||||
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
|
||||
|
||||
public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
|
||||
|
||||
@Override
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.jojubanking.boot.framework.jackson.core.databind;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* LocalDateTime序列化规则
|
||||
* <p>
|
||||
* 会将LocalDateTime序列化为毫秒级时间戳
|
||||
*/
|
||||
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||
|
||||
public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();
|
||||
|
||||
@Override
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.jojubanking.boot.framework.jackson.core;
|
||||
@@ -0,0 +1 @@
|
||||
package com.jojubanking.boot.framework;
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.jojubanking.boot.framework.swagger.config;
|
||||
|
||||
import com.jojubanking.boot.framework.swagger.core.SpringFoxHandlerProviderBeanPostProcessor;
|
||||
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
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 org.springframework.http.HttpHeaders;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.ExampleBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestParameterBuilder;
|
||||
import springfox.documentation.service.*;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static springfox.documentation.builders.RequestHandlerSelectors.basePackage;
|
||||
|
||||
/**
|
||||
* Swagger2 自动配置类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@Configuration
|
||||
@EnableSwagger2
|
||||
@EnableKnife4j
|
||||
@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
|
||||
// 允许使用 swagger.enable=false 禁用 Swagger
|
||||
@ConditionalOnProperty(prefix = "joju.swagger", value = "enable", matchIfMissing = true)
|
||||
@EnableConfigurationProperties(SwaggerProperties.class)
|
||||
public class JojuSwaggerAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public SpringFoxHandlerProviderBeanPostProcessor springFoxHandlerProviderBeanPostProcessor() {
|
||||
return new SpringFoxHandlerProviderBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Docket createRestApi(SwaggerProperties properties) {
|
||||
// 创建 Docket 对象
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
// ① 用来创建该 API 的基本信息,展示在文档的页面中(自定义展示的信息)
|
||||
.apiInfo(apiInfo(properties))
|
||||
// ② 设置扫描指定 package 包下的
|
||||
.select()
|
||||
.apis(basePackage(properties.getBasePackage()))
|
||||
// .apis(basePackage("com.jojubanking.boot.module.system")) // 可用于 swagger 无法展示时使用
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
// ③ 安全上下文(认证)
|
||||
.securitySchemes(securitySchemes())
|
||||
.securityContexts(securityContexts())
|
||||
// ④ 全局参数(多租户 header)
|
||||
.globalRequestParameters(globalRequestParameters());
|
||||
}
|
||||
|
||||
// ========== apiInfo ==========
|
||||
|
||||
/**
|
||||
* API 摘要信息
|
||||
*/
|
||||
private static ApiInfo apiInfo(SwaggerProperties properties) {
|
||||
return new ApiInfoBuilder()
|
||||
.title(properties.getTitle())
|
||||
.description(properties.getDescription())
|
||||
.contact(new Contact(properties.getAuthor(), null, null))
|
||||
.version(properties.getVersion())
|
||||
.build();
|
||||
}
|
||||
|
||||
// ========== securitySchemes ==========
|
||||
|
||||
/**
|
||||
* 安全模式,这里配置通过请求头 Authorization 传递 token 参数
|
||||
*/
|
||||
private static List<SecurityScheme> securitySchemes() {
|
||||
return Collections.singletonList(new ApiKey(HttpHeaders.AUTHORIZATION, "Authorization", "header"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全上下文
|
||||
*
|
||||
* @see #securitySchemes()
|
||||
* @see #authorizationScopes()
|
||||
*/
|
||||
private static List<SecurityContext> securityContexts() {
|
||||
return Collections.singletonList(SecurityContext.builder()
|
||||
.securityReferences(securityReferences())
|
||||
// 通过 PathSelectors.regex("^(?!auth).*$"),排除包含 "auth" 的接口不需要使用securitySchemes
|
||||
.operationSelector(o -> o.requestMappingPattern().matches("^(?!auth).*$"))
|
||||
.build());
|
||||
}
|
||||
|
||||
private static List<SecurityReference> securityReferences() {
|
||||
return Collections.singletonList(new SecurityReference(HttpHeaders.AUTHORIZATION, authorizationScopes()));
|
||||
}
|
||||
|
||||
private static AuthorizationScope[] authorizationScopes() {
|
||||
return new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")};
|
||||
}
|
||||
|
||||
// ========== globalRequestParameters ==========
|
||||
|
||||
private static List<RequestParameter> globalRequestParameters() {
|
||||
RequestParameterBuilder tenantParameter = new RequestParameterBuilder()
|
||||
.name(WebFrameworkUtils.HEADER_TENANT_ID).description("租户编号")
|
||||
.in(ParameterType.HEADER).example(new ExampleBuilder().value(1L).build());
|
||||
return Collections.singletonList(tenantParameter.build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jojubanking.boot.framework.swagger.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* Swagger 配置属性
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@ConfigurationProperties("joju.swagger")
|
||||
@Data
|
||||
public class SwaggerProperties {
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@NotEmpty(message = "标题不能为空")
|
||||
private String title;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@NotEmpty(message = "描述不能为空")
|
||||
private String description;
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
@NotEmpty(message = "作者不能为空")
|
||||
private String author;
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
@NotEmpty(message = "版本不能为空")
|
||||
private String version;
|
||||
/**
|
||||
* 扫描的包
|
||||
*/
|
||||
@NotEmpty(message = "扫描的 package 不能为空")
|
||||
private String basePackage;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.jojubanking.boot.framework.swagger.core;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.jojubanking.boot.framework.common.util.collection.CollectionUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
|
||||
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题
|
||||
* 该问题对应的 issue 为 https://github.com/springfox/springfox/issues/3462
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class SpringFoxHandlerProviderBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
|
||||
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
|
||||
// 移除,只保留 patternParser
|
||||
List<T> copy = CollectionUtils.filterList(mappings, mapping -> mapping.getPatternParser() == null);
|
||||
// 添加到 mappings 中
|
||||
mappings.clear();
|
||||
mappings.addAll(copy);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
|
||||
return (List<RequestMappingInfoHandlerMapping>)
|
||||
ReflectUtil.getFieldValue(bean, "handlerMappings");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 基于 Swagger + Knife4j 实现 API 接口文档
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
package com.jojubanking.boot.framework.swagger;
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.jojubanking.boot.framework.web.config;
|
||||
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import com.jojubanking.boot.framework.common.enums.WebFilterOrderEnum;
|
||||
import com.jojubanking.boot.framework.web.core.filter.CacheRequestBodyFilter;
|
||||
import com.jojubanking.boot.framework.web.core.filter.DemoFilter;
|
||||
import com.jojubanking.boot.framework.web.core.filter.XssFilter;
|
||||
import com.jojubanking.boot.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import com.jojubanking.boot.framework.web.core.handler.GlobalResponseBodyHandler;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({WebProperties.class, XssProperties.class})
|
||||
public class JojuWebAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Resource
|
||||
private WebProperties webProperties;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@Value("${spring.application.name}")
|
||||
private String applicationName;
|
||||
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurePathMatch(configurer, webProperties.getAdminApi());
|
||||
configurePathMatch(configurer, webProperties.getAppApi());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 API 前缀,仅仅匹配 controller 包下的
|
||||
*
|
||||
* @param configurer 配置
|
||||
* @param api API 配置
|
||||
*/
|
||||
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
|
||||
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
|
||||
configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class)
|
||||
&& antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) {
|
||||
return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GlobalResponseBodyHandler globalResponseBodyHandler() {
|
||||
return new GlobalResponseBodyHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) {
|
||||
// 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean
|
||||
return new WebFrameworkUtils(webProperties);
|
||||
}
|
||||
|
||||
// ========== Filter 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建 CorsFilter Bean,解决跨域问题
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<CorsFilter> corsFilterBean() {
|
||||
// 创建 CorsConfiguration 对象
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedOriginPattern("*"); // 设置访问源地址
|
||||
config.addAllowedHeader("*"); // 设置访问源请求头
|
||||
config.addAllowedMethod("*"); // 设置访问源请求方法
|
||||
// 创建 UrlBasedCorsConfigurationSource 对象
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置
|
||||
return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 RequestBodyCacheFilter Bean,可重复读取请求内容
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<CacheRequestBodyFilter> requestBodyCacheFilter() {
|
||||
return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 XssFilter Bean,解决 Xss 安全问题
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 DemoFilter Bean,演示模式
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "joju.demo", havingValue = "true")
|
||||
public FilterRegistrationBean<DemoFilter> demoFilter() {
|
||||
return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.jojubanking.boot.framework.web.config;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@ConfigurationProperties(prefix = "joju.web")
|
||||
@Validated
|
||||
@Data
|
||||
public class WebProperties {
|
||||
|
||||
@NotNull(message = "APP API 不能为空")
|
||||
private Api appApi = new Api("/smapp-api", "**.controller.app.**");
|
||||
@NotNull(message = "Admin API 不能为空")
|
||||
private Api adminApi = new Api("/smadmin-api", "**.controller.admin.**");
|
||||
|
||||
@NotNull(message = "Admin UI 不能为空")
|
||||
private Ui adminUi;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Valid
|
||||
public static class Api {
|
||||
|
||||
/**
|
||||
* API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
|
||||
*
|
||||
*
|
||||
* 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
|
||||
* 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
|
||||
*
|
||||
* @see JojuWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
|
||||
*/
|
||||
@NotEmpty(message = "API 前缀不能为空")
|
||||
private String prefix;
|
||||
|
||||
/**
|
||||
* Controller 所在包的 Ant 路径规则
|
||||
*
|
||||
* 主要目的是,给该 Controller 设置指定的 {@link #prefix}
|
||||
*/
|
||||
@NotEmpty(message = "Controller 所在包不能为空")
|
||||
private String controller;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Valid
|
||||
public static class Ui {
|
||||
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
private String url;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.jojubanking.boot.framework.web.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Xss 配置属性
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "joju.xss")
|
||||
@Validated
|
||||
@Data
|
||||
public class XssProperties {
|
||||
|
||||
/**
|
||||
* 是否开启,默认为 true
|
||||
*/
|
||||
private boolean enable = true;
|
||||
/**
|
||||
* 需要排除的 URL,默认为空
|
||||
*/
|
||||
private List<String> excludeUrls = Collections.emptyList();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.jojubanking.boot.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.web.config.WebProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 过滤 /admin-api、/app-api 等 API 请求的过滤器
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ApiRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
protected final WebProperties webProperties;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(),
|
||||
webProperties.getAppApi().getPrefix());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.jojubanking.boot.framework.web.core.filter;
|
||||
|
||||
import com.jojubanking.boot.framework.common.util.servlet.ServletUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Filter,实现它的可重复读取
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class CacheRequestBodyFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new CacheRequestBodyWrapper(request), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只处理 json 请求内容
|
||||
return !ServletUtils.isJsonRequest(request);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.jojubanking.boot.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Request Body 缓存 Wrapper
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class CacheRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 缓存的内容
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
public CacheRequestBodyWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
body = ServletUtil.getBodyBytes(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return inputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.jojubanking.boot.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.common.util.servlet.ServletUtils;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY;
|
||||
|
||||
/**
|
||||
* 演示 Filter,禁止用户发起写操作,避免影响测试数据
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class DemoFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String method = request.getMethod();
|
||||
return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率
|
||||
|| WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
|
||||
// 直接返回 DEMO_DENY 的结果。即,请求不继续
|
||||
ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.jojubanking.boot.framework.web.core.filter;
|
||||
|
||||
import com.jojubanking.boot.framework.web.config.XssProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Xss 过滤器
|
||||
*
|
||||
* 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class XssFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* 属性
|
||||
*/
|
||||
private final XssProperties properties;
|
||||
/**
|
||||
* 路径匹配器
|
||||
*/
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new XssRequestWrapper(request), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 如果关闭,则不过滤
|
||||
if (!properties.isEnable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果匹配到无需过滤,则不过滤
|
||||
String uri = request.getRequestURI();
|
||||
return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.jojubanking.boot.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HTMLFilter;
|
||||
import com.jojubanking.boot.framework.common.util.servlet.ServletUtils;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Xss 请求 Wrapper
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 基于线程级别的 HTMLFilter 对象,因为它线程非安全
|
||||
*/
|
||||
private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
|
||||
HTMLFilter htmlFilter = new HTMLFilter();
|
||||
// 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符
|
||||
ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
|
||||
return htmlFilter;
|
||||
});
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
private static String filterXss(String content) {
|
||||
if (StrUtil.isEmpty(content)) {
|
||||
return content;
|
||||
}
|
||||
return HTML_FILTER.get().filter(content);
|
||||
}
|
||||
|
||||
// ========== IO 流相关 ==========
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
// 如果非 json 请求,不进行 Xss 处理
|
||||
if (!ServletUtils.isJsonRequest(this)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
// 读取内容,并过滤
|
||||
String content = IoUtil.readUtf8(super.getInputStream());
|
||||
content = filterXss(content);
|
||||
final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return newInputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Param 相关 ==========
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return filterXss(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (ArrayUtil.isEmpty(values)) {
|
||||
return values;
|
||||
}
|
||||
// 过滤处理
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> valueMap = super.getParameterMap();
|
||||
if (CollUtil.isEmpty(valueMap)) {
|
||||
return valueMap;
|
||||
}
|
||||
// 过滤处理
|
||||
for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
}
|
||||
|
||||
// ========== Header 相关 ==========
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return filterXss(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.jojubanking.boot.framework.web.core.handler;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiErrorLog;
|
||||
import com.jojubanking.boot.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import com.jojubanking.boot.framework.common.exception.ServiceException;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
|
||||
import com.jojubanking.boot.framework.common.util.monitor.TracerUtils;
|
||||
import com.jojubanking.boot.framework.common.util.servlet.ServletUtils;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.ValidationException;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
|
||||
|
||||
/**
|
||||
* 处理所有异常,主要是提供给 Filter 使用
|
||||
* 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
|
||||
*
|
||||
* @param request 请求
|
||||
* @param ex 异常
|
||||
* @return 通用返回
|
||||
*/
|
||||
public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
|
||||
if (ex instanceof MissingServletRequestParameterException) {
|
||||
return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
|
||||
}
|
||||
if (ex instanceof MethodArgumentTypeMismatchException) {
|
||||
return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
|
||||
}
|
||||
if (ex instanceof MethodArgumentNotValidException) {
|
||||
return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
|
||||
}
|
||||
if (ex instanceof BindException) {
|
||||
return bindExceptionHandler((BindException) ex);
|
||||
}
|
||||
if (ex instanceof ConstraintViolationException) {
|
||||
return constraintViolationExceptionHandler((ConstraintViolationException) ex);
|
||||
}
|
||||
if (ex instanceof ValidationException) {
|
||||
return validationException((ValidationException) ex);
|
||||
}
|
||||
if (ex instanceof NoHandlerFoundException) {
|
||||
return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
|
||||
}
|
||||
if (ex instanceof HttpRequestMethodNotSupportedException) {
|
||||
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
|
||||
}
|
||||
if (ex instanceof RequestNotPermitted) {
|
||||
return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex);
|
||||
}
|
||||
if (ex instanceof ServiceException) {
|
||||
return serviceExceptionHandler((ServiceException) ex);
|
||||
}
|
||||
if (ex instanceof AccessDeniedException) {
|
||||
return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
|
||||
}
|
||||
return defaultExceptionHandler(request, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求参数缺失
|
||||
*
|
||||
* 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
|
||||
*/
|
||||
@ExceptionHandler(value = MissingServletRequestParameterException.class)
|
||||
public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
|
||||
log.warn("[missingServletRequestParameterExceptionHandler]", ex);
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求参数类型错误
|
||||
*
|
||||
* 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
|
||||
log.warn("[missingServletRequestParameterExceptionHandler]", ex);
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 参数校验不正确
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
|
||||
log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
|
||||
FieldError fieldError = ex.getBindingResult().getFieldError();
|
||||
assert fieldError != null; // 断言,避免告警
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public CommonResult<?> bindExceptionHandler(BindException ex) {
|
||||
log.warn("[handleBindException]", ex);
|
||||
FieldError fieldError = ex.getFieldError();
|
||||
assert fieldError != null; // 断言,避免告警
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Validator 校验不通过产生的异常
|
||||
*/
|
||||
@ExceptionHandler(value = ConstraintViolationException.class)
|
||||
public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
|
||||
log.warn("[constraintViolationExceptionHandler]", ex);
|
||||
ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
|
||||
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
|
||||
*/
|
||||
@ExceptionHandler(value = ValidationException.class)
|
||||
public CommonResult<?> validationException(ValidationException ex) {
|
||||
log.warn("[constraintViolationExceptionHandler]", ex);
|
||||
// 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
|
||||
return CommonResult.error(BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求地址不存在
|
||||
*
|
||||
* 注意,它需要设置如下两个配置项:
|
||||
* 1. spring.mvc.throw-exception-if-no-handler-found 为 true
|
||||
* 2. spring.mvc.static-path-pattern 为 /statics/**
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
|
||||
log.warn("[noHandlerFoundExceptionHandler]", ex);
|
||||
return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SpringMVC 请求方法不正确
|
||||
*
|
||||
* 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
|
||||
log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
|
||||
return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Resilience4j 限流抛出的异常
|
||||
*/
|
||||
@ExceptionHandler(value = RequestNotPermitted.class)
|
||||
public CommonResult<?> requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) {
|
||||
log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex);
|
||||
return CommonResult.error(TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Spring Security 权限不足的异常
|
||||
*
|
||||
* 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截
|
||||
*/
|
||||
@ExceptionHandler(value = AccessDeniedException.class)
|
||||
public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
|
||||
log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req),
|
||||
req.getRequestURL(), ex);
|
||||
return CommonResult.error(FORBIDDEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常 ServiceException
|
||||
*
|
||||
* 例如说,商品库存不足,用户手机号已存在。
|
||||
*/
|
||||
@ExceptionHandler(value = ServiceException.class)
|
||||
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
|
||||
log.info("[serviceExceptionHandler]", ex);
|
||||
return CommonResult.error(ex.getCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常,兜底处理所有的一切
|
||||
*/
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
|
||||
log.error("[defaultExceptionHandler]", ex);
|
||||
// 插入异常日志
|
||||
this.createExceptionLog(req, ex);
|
||||
// 返回 ERROR CommonResult
|
||||
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
|
||||
}
|
||||
|
||||
private void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// 插入错误日志
|
||||
ApiErrorLog errorLog = new ApiErrorLog();
|
||||
try {
|
||||
// 初始化 errorLog
|
||||
initExceptionLog(errorLog, req, e);
|
||||
// 执行插入 errorLog
|
||||
apiErrorLogFrameworkService.createApiErrorLog(errorLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) {
|
||||
// 处理用户信息
|
||||
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
|
||||
// 设置异常字段
|
||||
errorLog.setExceptionName(e.getClass().getName());
|
||||
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
|
||||
errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
|
||||
errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e));
|
||||
StackTraceElement[] stackTraceElements = e.getStackTrace();
|
||||
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
|
||||
StackTraceElement stackTraceElement = stackTraceElements[0];
|
||||
errorLog.setExceptionClassName(stackTraceElement.getClassName());
|
||||
errorLog.setExceptionFileName(stackTraceElement.getFileName());
|
||||
errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
|
||||
errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
|
||||
// 设置其它字段
|
||||
errorLog.setTraceId(TracerUtils.getTraceId());
|
||||
errorLog.setApplicationName(applicationName);
|
||||
errorLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
.put("query", ServletUtil.getParamMap(request))
|
||||
.put("body", ServletUtil.getBody(request)).build();
|
||||
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
errorLog.setRequestMethod(request.getMethod());
|
||||
errorLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
errorLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
errorLog.setExceptionTime(new Date());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.jojubanking.boot.framework.web.core.handler;
|
||||
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.jojubanking.boot.framework.apilog.core.filter.ApiAccessLogFilter;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 全局响应结果(ResponseBody)处理器
|
||||
*
|
||||
* 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult},
|
||||
* 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。
|
||||
* 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构
|
||||
*
|
||||
* 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果,
|
||||
* 方便 {@link ApiAccessLogFilter} 记录访问日志
|
||||
*/
|
||||
@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 结果
|
||||
WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult<?>) body);
|
||||
return body;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.jojubanking.boot.framework.web.core.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.jojubanking.boot.framework.common.enums.UserTypeEnum;
|
||||
import com.jojubanking.boot.framework.common.pojo.CommonResult;
|
||||
import com.jojubanking.boot.framework.web.config.WebProperties;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 专属于 web 包的工具类
|
||||
*
|
||||
* @author TW
|
||||
*/
|
||||
public class WebFrameworkUtils {
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id";
|
||||
private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type";
|
||||
|
||||
private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result";
|
||||
|
||||
public static final String HEADER_TENANT_ID = "tenant-id";
|
||||
|
||||
private static WebProperties properties;
|
||||
|
||||
public WebFrameworkUtils(WebProperties webProperties) {
|
||||
WebFrameworkUtils.properties = webProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得租户编号,从 header 中
|
||||
* 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 租户编号
|
||||
*/
|
||||
public static Long getTenantId(HttpServletRequest request) {
|
||||
String tenantId = request.getHeader(HEADER_TENANT_ID);
|
||||
return StrUtil.isNotEmpty(tenantId) ? Long.valueOf(tenantId) : null;
|
||||
}
|
||||
|
||||
public static void setLoginUserId(ServletRequest request, Long userId) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户类型
|
||||
*
|
||||
* @param request 请求
|
||||
* @param userType 用户类型
|
||||
*/
|
||||
public static void setLoginUserType(ServletRequest request, Integer userType) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号,从请求中
|
||||
* 注意:该方法仅限于 framework 框架使用!!!
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Long getLoginUserId(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的类型
|
||||
* 注意:该方法仅限于 web 相关的 framework 组件使用!!!
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Integer getLoginUserType(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
// 1. 优先,从 Attribute 中获取
|
||||
Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
|
||||
if (userType != null) {
|
||||
return userType;
|
||||
}
|
||||
// 2. 其次,基于 URL 前缀的约定
|
||||
if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) {
|
||||
return UserTypeEnum.ADMIN.getValue();
|
||||
}
|
||||
if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) {
|
||||
return UserTypeEnum.MEMBER.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Integer getLoginUserType() {
|
||||
HttpServletRequest request = getRequest();
|
||||
return getLoginUserType(request);
|
||||
}
|
||||
|
||||
public static Long getLoginUserId() {
|
||||
HttpServletRequest request = getRequest();
|
||||
return getLoginUserId(request);
|
||||
}
|
||||
|
||||
public static void setCommonResult(ServletRequest request, CommonResult<?> result) {
|
||||
request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result);
|
||||
}
|
||||
|
||||
public static CommonResult<?> getCommonResult(ServletRequest request) {
|
||||
return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
|
||||
}
|
||||
|
||||
public static HttpServletRequest getRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
return null;
|
||||
}
|
||||
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
|
||||
return servletRequestAttributes.getRequest();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 针对 SpringMVC 的基础封装
|
||||
*/
|
||||
package com.jojubanking.boot.framework.web;
|
||||
@@ -0,0 +1,5 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.jojubanking.boot.framework.apilog.config.JojuApiLogAutoConfiguration,\
|
||||
com.jojubanking.boot.framework.jackson.config.JojuJacksonAutoConfiguration,\
|
||||
com.jojubanking.boot.framework.swagger.config.JojuSwaggerAutoConfiguration,\
|
||||
com.jojubanking.boot.framework.web.config.JojuWebAutoConfiguration
|
||||
@@ -0,0 +1,5 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
com.jojubanking.boot.framework.apilog.config.JojuApiLogAutoConfiguration,\
|
||||
com.jojubanking.boot.framework.jackson.config.JojuJacksonAutoConfiguration,\
|
||||
com.jojubanking.boot.framework.swagger.config.JojuSwaggerAutoConfiguration,\
|
||||
com.jojubanking.boot.framework.web.config.JojuWebAutoConfiguration
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Wed Apr 02 13:07:27 CST 2025
|
||||
version=2.0.0-beta
|
||||
groupId=com.jojubanking.boot
|
||||
artifactId=joju-spring-boot-starter-web
|
||||
@@ -0,0 +1,31 @@
|
||||
com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyWrapper.class
|
||||
com\jojubanking\boot\framework\jackson\config\JojuJacksonAutoConfiguration.class
|
||||
com\jojubanking\boot\framework\web\core\filter\XssFilter.class
|
||||
com\jojubanking\boot\framework\apilog\core\service\ApiErrorLogFrameworkServiceImpl.class
|
||||
com\jojubanking\boot\framework\web\core\filter\ApiRequestFilter.class
|
||||
com\jojubanking\boot\framework\jackson\config\JojuJacksonAutoConfiguration$1.class
|
||||
com\jojubanking\boot\framework\web\core\handler\GlobalExceptionHandler.class
|
||||
com\jojubanking\boot\framework\apilog\core\service\ApiErrorLogFrameworkService.class
|
||||
com\jojubanking\boot\framework\jackson\core\databind\LocalDateTimeSerializer.class
|
||||
com\jojubanking\boot\framework\web\config\WebProperties$Ui.class
|
||||
com\jojubanking\boot\framework\apilog\config\JojuApiLogAutoConfiguration.class
|
||||
com\jojubanking\boot\framework\apilog\core\filter\ApiAccessLogFilter.class
|
||||
com\jojubanking\boot\framework\web\config\JojuWebAutoConfiguration.class
|
||||
com\jojubanking\boot\framework\swagger\core\SpringFoxHandlerProviderBeanPostProcessor.class
|
||||
com\jojubanking\boot\framework\swagger\config\SwaggerProperties.class
|
||||
com\jojubanking\boot\framework\web\config\WebProperties$Api.class
|
||||
com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyWrapper$1.class
|
||||
com\jojubanking\boot\framework\web\core\filter\DemoFilter.class
|
||||
com\jojubanking\boot\framework\web\core\handler\GlobalResponseBodyHandler.class
|
||||
com\jojubanking\boot\framework\web\core\util\WebFrameworkUtils.class
|
||||
com\jojubanking\boot\framework\apilog\core\service\ApiAccessLogFrameworkServiceImpl.class
|
||||
com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyFilter.class
|
||||
com\jojubanking\boot\framework\jackson\core\databind\LocalDateTimeDeserializer.class
|
||||
com\jojubanking\boot\framework\web\core\filter\XssRequestWrapper.class
|
||||
com\jojubanking\boot\framework\web\config\XssProperties.class
|
||||
com\jojubanking\boot\framework\apilog\core\service\ApiAccessLog.class
|
||||
com\jojubanking\boot\framework\apilog\core\service\ApiErrorLog.class
|
||||
com\jojubanking\boot\framework\web\core\filter\XssRequestWrapper$1.class
|
||||
com\jojubanking\boot\framework\apilog\core\service\ApiAccessLogFrameworkService.class
|
||||
com\jojubanking\boot\framework\swagger\config\JojuSwaggerAutoConfiguration.class
|
||||
com\jojubanking\boot\framework\web\config\WebProperties.class
|
||||
@@ -0,0 +1,31 @@
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiAccessLogFrameworkService.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\package-info.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyFilter.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiErrorLogFrameworkServiceImpl.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyWrapper.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\config\JojuJacksonAutoConfiguration.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\core\SpringFoxHandlerProviderBeanPostProcessor.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\core\package-info.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\DemoFilter.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\handler\GlobalResponseBodyHandler.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\package-info.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\config\JojuSwaggerAutoConfiguration.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\config\WebProperties.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\config\JojuApiLogAutoConfiguration.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\ApiRequestFilter.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiErrorLog.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\XssFilter.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiAccessLogFrameworkServiceImpl.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\XssRequestWrapper.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\util\WebFrameworkUtils.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\config\XssProperties.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiErrorLogFrameworkService.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\package-info.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiAccessLog.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\filter\ApiAccessLogFilter.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\package-info.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\config\JojuWebAutoConfiguration.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\core\databind\LocalDateTimeSerializer.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\config\SwaggerProperties.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\core\databind\LocalDateTimeDeserializer.java
|
||||
D:\^新版项目\05.九聚项目\32.库尔勒妇幼二期\下园体检\examination\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\handler\GlobalExceptionHandler.java
|
||||
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Swagger/?joju>
|
||||
@@ -0,0 +1 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?joju>
|
||||
Reference in New Issue
Block a user