init version

This commit is contained in:
terry.wang
2025-12-08 17:05:28 +08:00
commit ce3b71964a
4669 changed files with 224358 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

View 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>

View File

@@ -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;
}
}

View File

@@ -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()));
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
/**
* API 日志:包含两类
* 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
* 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
*
* @author TW
*/
package com.jojubanking.boot.framework.apilog;

View File

@@ -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;
}
};
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1 @@
package com.jojubanking.boot.framework.jackson.core;

View File

@@ -0,0 +1 @@
package com.jojubanking.boot.framework;

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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");
}
}

View File

@@ -0,0 +1,6 @@
/**
* 基于 Swagger + Knife4j 实现 API 接口文档
*
* @author TW
*/
package com.jojubanking.boot.framework.swagger;

View File

@@ -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;
}
}

View File

@@ -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("/kfapp-api", "**.controller.app.**");
@NotNull(message = "Admin API 不能为空")
private Api adminApi = new Api("/kfadmin-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;
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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避免 " 被转移成 &quot; 字符
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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,4 @@
/**
* 针对 SpringMVC 的基础封装
*/
package com.jojubanking.boot.framework.web;

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,5 @@
#Generated by Maven
#Fri Aug 16 12:45:48 CST 2024
version=2.0.0-beta
groupId=com.jojubanking.boot
artifactId=joju-spring-boot-starter-web

View File

@@ -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

View File

@@ -0,0 +1,31 @@
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\config\XssProperties.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\package-info.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiAccessLogFrameworkService.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\filter\ApiAccessLogFilter.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\config\JojuWebAutoConfiguration.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\handler\GlobalExceptionHandler.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\config\WebProperties.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\config\SwaggerProperties.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\config\JojuApiLogAutoConfiguration.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiErrorLog.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\util\WebFrameworkUtils.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\package-info.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyWrapper.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\handler\GlobalResponseBodyHandler.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\XssFilter.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiAccessLogFrameworkServiceImpl.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\CacheRequestBodyFilter.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiErrorLogFrameworkService.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\XssRequestWrapper.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\core\SpringFoxHandlerProviderBeanPostProcessor.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\ApiRequestFilter.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\core\databind\LocalDateTimeDeserializer.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\swagger\config\JojuSwaggerAutoConfiguration.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiErrorLogFrameworkServiceImpl.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\core\package-info.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\core\databind\LocalDateTimeSerializer.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\package-info.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\web\core\filter\DemoFilter.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\core\service\ApiAccessLog.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\apilog\package-info.java
E:\D-Springboot\kfdj\kfdj\joju-framework\joju-spring-boot-starter-web\src\main\java\com\jojubanking\boot\framework\jackson\config\JojuJacksonAutoConfiguration.java

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Swagger/?joju>

View File

@@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/SpringMVC/?joju>