跳到主要内容
版本:Next

framework-starter-web - Web 功能自动配置模块

1. 模块概述

framework-starter-web 是 epoch-framework 中的 Web 功能自动配置模块,集成了 Spring MVC、Swagger (Knife4j)、全局异常处理、统一响应格式等核心功能。该模块通过自动配置的方式,简化了 Web 应用的开发,提供了标准化的 API 开发规范和最佳实践。

2. 模块结构

framework-starter-web/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/epoch/framework/starter/web/
│ │ │ ├── config/ # 自动配置类
│ │ │ ├── exception/ # 全局异常处理
│ │ │ ├── filter/ # 过滤器实现
│ │ │ ├── interceptor/ # 拦截器实现
│ │ │ ├── response/ # 统一响应格式
│ │ │ ├── validation/ # 参数验证配置
│ │ │ ├── swagger/ # Swagger 配置
│ │ │ └── support/ # 支持工具类
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring.factories # Spring Boot 自动配置入口
│ └── test/ # 测试代码
└── pom.xml # Maven 依赖配置

3. 核心功能

3.1 Spring MVC 自动配置

  • 自动配置 Spring MVC 核心组件
  • 支持视图解析器和消息转换器配置
  • 提供静态资源访问支持
  • 集成 ContentNegotiation 和 MediaType 配置

3.2 Swagger (Knife4j) API 文档

  • 自动配置 Swagger 3.0 和 Knife4j 增强
  • 提供 API 文档自动生成功能
  • 支持接口调试和在线测试
  • 提供接口版本管理和分组功能

3.3 跨域支持自动配置

  • 自动配置 CORS 跨域支持
  • 支持自定义跨域配置
  • 提供细粒度的跨域控制
  • 支持预请求 (Preflight) 缓存

3.4 拦截器自动配置

  • 提供全局拦截器配置
  • 支持请求日志记录
  • 提供请求耗时统计
  • 支持权限验证和访问控制

3.5 过滤器自动配置

  • 自动配置常用过滤器
  • 支持 XSS 防护过滤器
  • 提供请求参数过滤
  • 支持字符编码过滤器

3.6 全局异常处理

  • 统一处理应用异常
  • 提供标准化的错误响应
  • 支持自定义异常类型
  • 提供异常日志记录

3.7 统一响应格式

  • 提供全局响应包装
  • 支持标准化的响应结构
  • 提供分页响应封装
  • 支持响应状态码映射

3.8 参数验证配置

  • 集成 JSR-303/JSR-380 参数验证
  • 提供自定义验证注解
  • 支持分组验证和顺序验证
  • 提供友好的验证错误提示

4. 配置说明

4.1 Spring MVC 配置

spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
view:
prefix: /WEB-INF/views/
suffix: .jsp
static-path-pattern: /static/**
contentnegotiation:
favor-parameter: false
favor-path-extension: false
throw-exception-if-no-handler-found: true
web:
resources:
static-locations: classpath:/static/,classpath:/public/,classpath:/resources/

4.2 Swagger (Knife4j) 配置

knife4j:
enable: true
openapi:
title: Epoch API 文档
description: Epoch 平台 API 接口文档
version: 1.0.0
contact:
name: Epoch Team
email: admin@epoch.com
license:
name: Apache version-v2
url: http://www.apache.org/licenses/LICENSE-2.0
servers:
- url: http://localhost:8080
description: 开发环境
- url: http://test.epoch.com
description: 测试环境
- url: http://prod.epoch.com
description: 生产环境
setting:
language: zh_cn
enable-swagger-models: true
swagger-model-name: 数据模型
enable-document-manage: true
enable-after-script: true
enable-version: true
enable-filter-multipart-api-method-type: GET
cors:
enabled: true

4.3 跨域配置

epoch:
framework:
web:
cors:
enabled: true
allowed-origins: '*'
allowed-methods: GET,POST,PUT,DELETE,OPTIONS
allowed-headers: '*'
exposed-headers: Content-Disposition
allow-credentials: true
max-age: 3600

4.4 全局异常处理配置

epoch:
framework:
web:
exception:
enabled: true
show-details: true
log-exceptions: true
default-error-code: 500
default-error-message: 服务器内部错误

4.5 统一响应配置

epoch:
framework:
web:
response:
enabled: true
wrap-success-response: true
default-success-message: 操作成功
default-error-message: 操作失败
page:
page-param-name: page
size-param-name: size
total-field-name: total
records-field-name: records

4.6 参数验证配置

epoch:
framework:
web:
validation:
enabled: true
message-source: messages
default-message: 参数验证失败
locale: zh_CN

5. 使用指南

5.1 引入依赖

<dependency>
<groupId>com.epoch</groupId>
<artifactId>framework-starter-web</artifactId>
<version>${epoch.version}</version>
</dependency>

5.2 控制器开发

@RestController
@RequestMapping("/api/v1/users")
@Api(tags = "用户管理")
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/{id}")
@ApiOperation("根据ID获取用户信息")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataTypeClass = Long.class)
public ResponseResult<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseResult.success(user);
}

@GetMapping
@ApiOperation("获取用户列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "页码", defaultValue = "1", dataTypeClass = Integer.class),
@ApiImplicitParam(name = "size", value = "每页大小", defaultValue = "10", dataTypeClass = Integer.class),
@ApiImplicitParam(name = "username", value = "用户名", required = false, dataTypeClass = String.class)
})
public ResponseResult<PageResult<User>> getUserList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username) {
PageResult<User> userList = userService.getUserList(page, size, username);
return ResponseResult.success(userList);
}

@PostMapping
@ApiOperation("创建用户")
@ApiImplicitParam(name = "user", value = "用户信息", required = true, dataTypeClass = UserRequest.class)
public ResponseResult<Long> createUser(@Valid @RequestBody UserRequest user) {
Long userId = userService.createUser(user);
return ResponseResult.success(userId);
}

@PutMapping("/{id}")
@ApiOperation("更新用户")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataTypeClass = Long.class),
@ApiImplicitParam(name = "user", value = "用户信息", required = true, dataTypeClass = UserRequest.class)
})
public ResponseResult<Void> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserRequest user) {
userService.updateUser(id, user);
return ResponseResult.success();
}

@DeleteMapping("/{id}")
@ApiOperation("删除用户")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataTypeClass = Long.class)
public ResponseResult<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseResult.success();
}
}

5.3 请求参数验证

@Data
@ApiModel("用户请求参数")
public class UserRequest {

@ApiModelProperty(value = "用户名", required = true, minLength = 3, maxLength = 20)
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在 3-20 个字符之间")
private String username;

@ApiModelProperty(value = "密码", required = true, minLength = 6)
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码长度不能少于 6 个字符")
private String password;

@ApiModelProperty(value = "邮箱", required = true)
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;

@ApiModelProperty(value = "年龄", required = true, min = 1, max = 120)
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄不能小于 1 岁")
@Max(value = 120, message = "年龄不能大于 120 岁")
private Integer age;

@ApiModelProperty(value = "状态", required = true)
@NotNull(message = "状态不能为空")
@EnumValid(target = UserStatus.class, message = "状态值无效")
private Integer status;
}

5.4 自定义异常

@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {

private Integer code;
private String message;
private Object data;

public BusinessException(String message) {
super(message);
this.code = 500;
this.message = message;
}

public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}

public BusinessException(Integer code, String message, Object data) {
super(message);
this.code = code;
this.message = message;
this.data = data;
}
}

5.5 Swagger API 文档访问

  • 访问地址:http://localhost:8080/doc.html
  • API 文档 JSON:http://localhost:8080/v3/api-docs
  • API 分组文档:http://localhost:8080/v3/api-docs/group/default

5.6 统一响应格式使用

// 成功响应
return ResponseResult.success();
return ResponseResult.success(data);
return ResponseResult.success("操作成功", data);

// 失败响应
return ResponseResult.error();
return ResponseResult.error("操作失败");
return ResponseResult.error(500, "操作失败");
return ResponseResult.error(500, "操作失败", data);

// 分页响应
PageResult<User> pageResult = new PageResult<>(total, records);
return ResponseResult.success(pageResult);

6. 自定义配置

6.1 自定义拦截器

@Component
public class LoggingInterceptor implements HandlerInterceptor {

private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
private final ThreadLocal<Long> startTime = new ThreadLocal<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
startTime.set(System.currentTimeMillis());
logger.info("Request: {} {}", request.getMethod(), request.getRequestURI());
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 可以在这里添加响应处理逻辑
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long elapsedTime = System.currentTimeMillis() - startTime.get();
logger.info("Response: {} {} - {}ms", request.getMethod(), request.getRequestURI(), elapsedTime);
startTime.remove();
}
}

@Configuration
public class WebInterceptorConfig implements WebMvcConfigurer {

@Autowired
private LoggingInterceptor loggingInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/static/**", "/doc.html", "/v3/api-docs/**");
}
}

6.2 自定义过滤器

@Component
@WebFilter(urlPatterns = "/*", filterName = "XssFilter")
public class XssFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 过滤器初始化
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 使用 XSS 防护包装请求
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response);
}

@Override
public void destroy() {
// 过滤器销毁
}
}

6.3 自定义异常处理

@ControllerAdvice
public class CustomExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);

@ExceptionHandler(CustomBusinessException.class)
@ResponseBody
public ResponseResult handleCustomBusinessException(CustomBusinessException e) {
logger.error("CustomBusinessException: {}", e.getMessage(), e);
return ResponseResult.error(400, e.getMessage());
}

@ExceptionHandler(ResourceNotFoundException.class)
@ResponseBody
public ResponseResult handleResourceNotFoundException(ResourceNotFoundException e) {
logger.error("ResourceNotFoundException: {}", e.getMessage(), e);
return ResponseResult.error(404, e.getMessage());
}

@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseResult handleException(Exception e) {
logger.error("Exception: {}", e.getMessage(), e);
return ResponseResult.error(500, "服务器内部错误");
}
}

6.4 自定义参数验证器

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumValid {
String message() default "枚举值无效";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> target();
}

public class EnumValidator implements ConstraintValidator<EnumValid, Integer> {

private Class<? extends Enum<?>> target;

@Override
public void initialize(EnumValid constraintAnnotation) {
this.target = constraintAnnotation.target();
}

@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}

Enum<?>[] enumConstants = target.getEnumConstants();
for (Enum<?> enumConstant : enumConstants) {
if (enumConstant.ordinal() == value) {
return true;
}
}
return false;
}
}

6.5 自定义消息转换器

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 自定义 JSON 消息转换器
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();

// 配置日期格式化
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
objectMapper.registerModule(new JavaTimeModule());

// 配置 null 值处理
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

converter.setObjectMapper(objectMapper);

// 将自定义转换器添加到转换器列表的最前面
converters.add(0, converter);
}
}

7. 最佳实践

7.1 API 设计规范

  • 版本控制:使用 URL 路径版本控制(如 /api/v1/users
  • 资源命名:使用名词复数形式(如 /users/orders
  • HTTP 方法:遵循 RESTful 原则,使用 GET/POST/PUT/DELETE 等方法
  • 状态码:使用标准的 HTTP 状态码(如 200、400、404、500)
  • 参数传递:路径参数用于资源标识,查询参数用于过滤和分页

7.2 响应格式规范

  • 统一结构:使用标准的响应结构(如 {"code": 200, "message": "success", "data": {}}
  • 错误处理:提供详细的错误码和错误信息
  • 分页响应:统一分页参数(page/size)和响应结构(total/records)
  • 空值处理:避免返回 null 值,使用空数组或空对象代替

7.3 参数验证规范

  • 前端验证:提供即时的前端验证反馈
  • 后端验证:所有参数必须进行后端验证
  • 验证注解:使用 JSR-303/JSR-380 验证注解
  • 自定义验证:针对业务需求创建自定义验证注解
  • 错误提示:提供清晰、友好的验证错误提示

7.4 安全配置规范

  • 跨域配置:在生产环境中限制允许的来源
  • 请求过滤:防止 XSS、SQL 注入等攻击
  • 敏感信息:不在响应中暴露敏感信息
  • 接口保护:对敏感接口进行权限控制

7.5 性能优化建议

  • 合理使用缓存:对频繁访问的数据使用缓存
  • 异步处理:对耗时操作使用异步处理
  • 批量操作:减少数据库交互次数
  • 懒加载:避免加载不必要的数据
  • 响应压缩:启用 GZIP 等压缩算法

8. 常见问题

8.1 Swagger 文档无法访问

原因:Swagger 配置不正确或依赖缺失。

解决方案

  • 检查 Swagger (Knife4j) 依赖是否正确引入
  • 确保 Swagger 配置项 knife4j.enable 为 true
  • 检查是否有拦截器或过滤器阻止了 Swagger 访问
  • 查看应用日志,查找 Swagger 相关错误

8.2 跨域请求失败

原因:CORS 配置不正确或请求方法不被允许。

解决方案

  • 检查跨域配置项 epoch.framework.web.cors.enabled 是否为 true
  • 确保请求方法在允许的方法列表中
  • 检查是否设置了正确的允许来源、头部等配置
  • 查看浏览器控制台的 CORS 错误信息

8.3 全局异常处理不生效

原因:异常处理配置不正确或异常类型不匹配。

解决方案

  • 检查异常处理配置项 epoch.framework.web.exception.enabled 是否为 true
  • 确保异常处理类使用了 @ControllerAdvice 注解
  • 检查异常处理方法的 @ExceptionHandler 注解是否正确
  • 查看应用日志,确认异常是否被正确捕获

8.4 参数验证不生效

原因:参数验证配置不正确或验证注解使用错误。

解决方案

  • 确保在控制器方法参数上使用了 @Valid@Validated 注解
  • 检查验证注解是否正确应用在实体类字段上
  • 查看应用日志,确认验证错误是否被正确捕获
  • 检查是否引入了 JSR-303/JSR-380 验证依赖

8.5 统一响应格式不生效

原因:响应包装配置不正确或控制器方法返回类型不符合要求。

解决方案

  • 检查响应包装配置项 epoch.framework.web.response.enabled 是否为 true
  • 确保控制器方法返回类型为 ResponseResult 或其子类
  • 检查响应包装的实现类是否正确配置
  • 查看应用日志,确认响应是否被正确包装

9. 版本变更

9.1 主要变更

  • 3.4.0

    • 升级 Swagger 版本至 3.0,集成 Knife4j 4.6.0
    • 优化全局异常处理,支持更多异常类型
    • 完善统一响应格式,增加分页响应支持
    • 增强参数验证功能,支持分组验证
  • 3.3.0

    • 重构 Web 功能自动配置,提高可扩展性
    • 新增跨域配置自动配置
    • 优化拦截器和过滤器配置
    • 增加请求日志记录功能
  • 3.2.0

    • 初始版本,提供 Spring MVC、Swagger、全局异常处理、统一响应格式等功能