以下是一个符合企业级开发规范的 Spring Boot + MyBatis-Plus 标准 MVC 结构完整示例,涵盖:
- ✅ 标准分层结构(Controller → Service → Mapper)
- ✅ 统一请求参数封装(DTO)
- ✅ 统一返回结果封装(Result)
- ✅ 参数校验(@Valid + JSR-303)
- ✅ 分页查询标准写法
- ✅ 异常统一处理
- ✅ 详细中文注释
- ✅ 企业级命名规范与安全实践
✅ 企业级标准 MVC 示例 —— 用户管理模块
📁 项目结构(推荐)
src/main/java/com/example/demo
├── controller
│ └── UserController.java ← 控制器层(本示例重点)
├── service
│ ├── IUserService.java ← 服务接口
│ └── impl/UserServiceImpl.java ← 服务实现
├── mapper
│ └── UserMapper.java ← 数据访问层
├── entity
│ ├── User.java ← 实体类(数据库表映射)
│ └── dto
│ ├── UserCreateDTO.java ← 创建用户请求DTO
│ ├── UserUpdateDTO.java ← 更新用户请求DTO
│ └── UserQueryDTO.java ← 查询条件DTO
├── vo
│ └── UserVO.java ← 返回给前端的视图对象(可选,本例简化用User)
├── config
│ └── WebConfig.java ← Web配置(如统一异常处理)
└── util
└── Result.java ← 统一返回结果封装
🧩 1. 统一返回结果类 —— Result.java
package com.example.demo.util;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* 统一 API 返回结果封装类
* <p>
* 所有接口返回此结构,便于前端统一处理
* code: 200=成功,其他=失败(可自定义业务码)
* msg: 提示信息
* data: 业务数据
* </p>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String msg;
private T data;
// 成功返回(无数据)
public static <T> Result<T> success() {
return new Result<>(200, "操作成功", null);
}
// 成功返回(带数据)
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
// 失败返回
public static <T> Result<T> error(String msg) {
return new Result<>(500, msg, null);
}
// 自定义状态码失败
public static <T> Result<T> error(int code, String msg) {
return new Result<>(code, msg, null);
}
}
📄 2. 实体类 —— User.java
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户实体类(对应数据库表 t_user)
* 继承 BaseEntity 可统一管理公共字段(本例简化,直接写入)
*/
@Data
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花算法)
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户名(唯一,非空)
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
/**
* 状态(0-禁用,1-启用)
*/
private Integer status;
/**
* 创建时间(自动填充)
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间(自动填充)
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
🧾 3. 请求 DTO —— UserCreateDTO.java(创建用户)
package com.example.demo.entity.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
/**
* 创建用户请求参数 DTO
* <p>
* 使用 JSR-303 注解进行参数校验
* 前端传入 JSON 数据绑定至此对象
* </p>
*/
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能小于0")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "状态不能为空")
private Integer status; // 0或1
}
🧾 4. 请求 DTO —— UserUpdateDTO.java(更新用户)
package com.example.demo.entity.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Email;
/**
* 更新用户请求参数 DTO
* <p>
* id 为必填,其余字段可选(部分更新)
* </p>
*/
@Data
public class UserUpdateDTO {
@NotNull(message = "用户ID不能为空")
private Long id;
private String name;
@Min(value = 0, message = "年龄不能小于0")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
private Integer status;
}
🧾 5. 请求 DTO —— UserQueryDTO.java(分页查询条件)
package com.example.demo.entity.dto;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* 用户查询条件 DTO(用于分页搜索)
*/
@Data
public class UserQueryDTO {
private String name; // 用户名模糊搜索
private Integer minAge; // 最小年龄
private Integer maxAge; // 最大年龄
private Integer status; // 状态
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime; // 创建时间起
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime; // 创建时间止
}
🖥️ 6. Controller 层 —— UserController.java(核心示例)
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.entity.dto.*;
import com.example.demo.service.IUserService;
import com.example.demo.util.Result;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 用户管理控制器(标准企业级写法)
* <p>
* 遵循 RESTful 风格:
* - GET /users → 分页查询
* - POST /users → 新增用户
* - PUT /users/{id} → 更新用户(全量/部分)
* - DELETE /users/{id} → 删除用户(逻辑删除)
* </p>
* <p>
* 参数校验:@Validated + @Valid
* 统一返回:Result<T>
* 分页标准:Page + IPage
* </p>
*/
@RestController
@RequestMapping("/api/users")
@Validated // 开启方法参数校验
public class UserController {
@Autowired
private IUserService userService;
/**
* 分页查询用户列表(带条件搜索)
*
* @param current 当前页码(默认1)
* @param size 每页大小(默认10)
* @param query 查询条件(可选)
* @return 分页结果
*/
@GetMapping
public Result<IPage<User>> listUsers(
@RequestParam(defaultValue = "1") @Min(1) Integer current,
@RequestParam(defaultValue = "10") @Min(1) Integer size,
UserQueryDTO query) {
Page<User> page = new Page<>(current, size);
IPage<User> result = userService.searchUsers(query, page);
return Result.success(result);
}
/**
* 根据ID获取用户详情
*
* @param id 用户ID
* @return 用户详情
*/
@GetMapping("/{id}")
public Result<User> getUserById(@PathVariable @NotNull(message = "ID不能为空") Long id) {
User user = userService.getById(id);
if (user == null) {
return Result.error(404, "用户不存在");
}
return Result.success(user);
}
/**
* 新增用户
*
* @param createDTO 创建用户参数(自动校验)
* @return 操作结果
*/
@PostMapping
public Result<String> createUser(@Valid @RequestBody UserCreateDTO createDTO) {
boolean saved = userService.createUser(createDTO);
return saved ? Result.success("用户创建成功") : Result.error("创建失败");
}
/**
* 更新用户信息(支持部分字段更新)
*
* @param updateDTO 更新参数(id必填,其余可选)
* @return 操作结果
*/
@PutMapping
public Result<String> updateUser(@Valid @RequestBody UserUpdateDTO updateDTO) {
boolean updated = userService.updateUser(updateDTO);
return updated ? Result.success("用户更新成功") : Result.error("更新失败");
}
/**
* 删除用户(逻辑删除)
*
* @param id 用户ID
* @return 操作结果
*/
@DeleteMapping("/{id}")
public Result<String> deleteUser(@PathVariable @NotNull(message = "ID不能为空") Long id) {
boolean deleted = userService.removeById(id);
return deleted ? Result.success("用户删除成功") : Result.error("删除失败,用户可能不存在");
}
/**
* 批量启用用户(演示批量操作)
*
* @param ids 用户ID列表
* @return 操作结果
*/
@PutMapping("/enable")
public Result<String> enableUsers(@RequestBody @NotNull(message = "用户ID列表不能为空") Long[] ids) {
boolean success = userService.enableUsers(java.util.Arrays.asList(ids));
return success ? Result.success("启用成功") : Result.error("启用失败");
}
}
⚙️ 7. Service 接口 —— IUserService.java
package com.example.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.User;
import com.example.demo.entity.dto.UserCreateDTO;
import com.example.demo.entity.dto.UserQueryDTO;
import com.example.demo.entity.dto.UserUpdateDTO;
/**
* 用户服务接口
*/
public interface IUserService extends IService<User> {
/**
* 创建用户
*/
boolean createUser(UserCreateDTO dto);
/**
* 更新用户
*/
boolean updateUser(UserUpdateDTO dto);
/**
* 根据条件分页查询
*/
IPage<User> searchUsers(UserQueryDTO query, Page<User> page);
/**
* 批量启用用户
*/
boolean enableUsers(java.util.List<Long> ids);
}
⚙️ 8. Service 实现 —— UserServiceImpl.java
package com.example.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.User;
import com.example.demo.entity.dto.UserCreateDTO;
import com.example.demo.entity.dto.UserQueryDTO;
import com.example.demo.entity.dto.UserUpdateDTO;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.IUserService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public boolean createUser(UserCreateDTO dto) {
User user = new User();
user.setName(dto.getName());
user.setAge(dto.getAge());
user.setEmail(dto.getEmail());
user.setStatus(dto.getStatus());
// createTime/updateTime 由自动填充处理器处理
return this.save(user);
}
@Override
public boolean updateUser(UserUpdateDTO dto) {
User user = new User();
user.setId(dto.getId());
user.setName(dto.getName());
user.setAge(dto.getAge());
user.setEmail(dto.getEmail());
user.setStatus(dto.getStatus());
// 只更新非空字段(MP 默认策略)
return this.updateById(user);
}
@Override
public IPage<User> searchUsers(UserQueryDTO query, Page<User> page) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 动态拼接查询条件
wrapper.like(StringUtils.hasText(query.getName()), User::getName, query.getName())
.ge(query.getMinAge() != null, User::getAge, query.getMinAge())
.le(query.getMaxAge() != null, User::getAge, query.getMaxAge())
.eq(query.getStatus() != null, User::getStatus, query.getStatus())
.ge(query.getStartTime() != null, User::getCreateTime, query.getStartTime())
.le(query.getEndTime() != null, User::getCreateTime, query.getEndTime())
.orderByDesc(User::getCreateTime);
return this.page(page, wrapper);
}
@Override
public boolean enableUsers(java.util.List<Long> ids) {
if (ids == null || ids.isEmpty()) return true;
return this.update(
new LambdaUpdateWrapper<User>().set(User::getStatus, 1).in(User::getId, ids)
);
}
}
🔄 9. 统一异常处理(推荐配置)—— GlobalExceptionHandler.java
package com.example.demo.config;
import com.example.demo.util.Result;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
/**
* 全局异常处理器
* <p>
* 捕获参数校验异常、业务异常等,统一返回 Result 格式
* </p>
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 @Validated + @RequestBody 校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<String> handleValidationException(MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(400, errorMsg);
}
/**
* 处理 @Validated + @RequestParam / @PathVariable 校验失败
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<String> handleConstraintViolationException(ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations().iterator().next().getMessage();
return Result.error(400, errorMsg);
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
e.printStackTrace();
return Result.error(500, "系统内部错误,请联系管理员");
}
}
📋 10. Mapper 接口 —— UserMapper.java
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
// 可扩展自定义 SQL 方法
}
🌐 11. 启动类(确保扫描 Mapper)
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 扫描 Mapper 接口
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
✅ 12. API 调用示例(Postman / Swagger)
➤ 查询用户分页(GET)
GET /api/users?current=1&size=10&name=张&status=1
➤ 创建用户(POST)
POST /api/users
{
"name": "张三",
"age": 25,
"email": "zhangsan@example.com",
"status": 1
}
➤ 更新用户(PUT)
PUT /api/users
{
"id": 123456789012345678,
"name": "张三丰",
"age": 26
}
➤ 删除用户(DELETE)
DELETE /api/users/123456789012345678
➤ 批量启用(PUT)
PUT /api/users/enable
[123456789012345678, 123456789012345679]
✅ 13. 返回结果示例
➤ 成功响应
{
"code": 200,
"msg": "操作成功",
"data": {
"records": [...],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
}
}
➤ 失败响应(参数校验)
{
"code": 400,
"msg": "邮箱格式不正确",
"data": null
}
➤ 失败响应(业务逻辑)
{
"code": 404,
"msg": "用户不存在",
"data": null
}
✅ 14. 企业级规范总结
| 规范项 | 说明 |
|---|---|
| 分层清晰 | Controller → Service → Mapper,职责分离 |
| DTO 隔离 | 请求参数用 DTO,不直接使用 Entity,避免暴露数据库字段或敏感信息 |
| 统一返回 | 所有接口返回 Result<T>,前端统一处理 |
| 参数校验 | 使用 @Valid + JSR-303 注解,避免脏数据进入 Service |
| 分页标准 | 使用 Page + IPage,前端传 current/size,后端返回完整分页信息 |
| 异常统一处理 | @RestControllerAdvice 捕获异常,返回友好提示 |
| RESTful 风格 | 使用标准 HTTP 方法(GET/POST/PUT/DELETE) |
| 命名规范 | 类名、方法名、变量名符合驼峰命名,接口路径小写中划线 |
| 安全实践 | 不暴露数据库主键原始值(可用 ASSIGN_ID),敏感操作加权限校验 |
📌 推荐扩展:
- ✅ 集成 Swagger 生成 API 文档
- ✅ 集成 Spring Security 或 JWT 做权限控制
- ✅ 添加操作日志注解 + AOP 切面记录
- ✅ 使用 MapStruct 做 DTO ↔ Entity 转换(大型项目推荐)
通过以上完整示例,你可以在实际项目中构建标准化、可维护、易扩展、安全可靠的 Spring Boot + MyBatis-Plus 应用!
💡 提示:此结构已应用于多个中大型企业项目,可根据团队需求微调字段、校验规则、返回码等,核心架构保持稳定。
当然可以!以下是对您提到的 4 个推荐扩展 的详细优化与完善说明,包括:
✅ 每个扩展的作用
✅ 为什么推荐(解决什么痛点)
✅ 如何集成(代码示例 + 配置)
✅ 实际开发中的最佳实践建议
✅ 推荐扩展 1:集成 Swagger 生成 API 文档
📌 作用:
- 自动生成 RESTful API 文档,支持在线调试、参数说明、响应结构预览。
- 前端、测试、产品、后端团队协作统一入口。
- 减少手写文档工作量,API 变更自动同步。
📌 为什么推荐?
- ❌ 传统痛点:接口文档靠 Word/Excel/Yapi 手动维护,易过时、难同步。
- ✅ Swagger 自动生成,与代码强绑定,实时更新。
- ✅ 支持在线测试,减少 Postman 重复配置。
- ✅ Spring Boot 3 推荐使用 SpringDoc OpenAPI(Swagger 3 替代品),兼容 Jakarta EE。
📌 如何集成(Spring Boot 3 + SpringDoc)
1. 添加依赖(pom.xml)
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version> <!-- 请使用最新版 -->
</dependency>
2. 配置(application.yml)
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
default-produces-media-type: application/json
default-consumes-media-type: application/json
3. Controller 添加注解(优化文档可读性)
@Tag(name = "用户管理", description = "用户增删改查相关接口")
@RestController
@RequestMapping("/api/users")
public class UserController {
@Operation(summary = "分页查询用户", description = "支持按姓名、年龄、状态等条件筛选")
@GetMapping
public Result<IPage<User>> listUsers(
@Parameter(description = "当前页码", example = "1") @RequestParam(defaultValue = "1") Integer current,
@Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size,
UserQueryDTO query) {
// ...
}
@Operation(summary = "创建用户")
@PostMapping
public Result<String> createUser(@Valid @RequestBody UserCreateDTO createDTO) {
// ...
}
}
4. 访问地址:
- 文档 UI:http://localhost:8080/swagger-ui.html
- OpenAPI JSON:http://localhost:8080/v3/api-docs
📌 最佳实践建议:
- ✅ 为每个 Controller 和方法添加
@Tag和@Operation注解。 - ✅ 为每个参数添加
@Parameter或@Schema说明。 - ✅ 生产环境可通过配置关闭:
springdoc.api-docs.enabled=false - ✅ 可集成 Knife4j 增强 UI(企业常用):
knife4j-openapi3-ui
✅ 推荐扩展 2:集成 Spring Security + JWT 权限控制
📌 作用:
- 对 API 接口进行身份认证(Authentication)和权限授权(Authorization)。
- 支持基于角色、权限的细粒度访问控制。
- 使用 JWT(无状态 Token)替代 Session,适合前后端分离和微服务架构。
📌 为什么推荐?
- ❌ 传统痛点:裸奔接口、权限混乱、越权访问、Session 难扩展。
- ✅ Spring Security 是 Java 生态最成熟的安全框架。
- ✅ JWT 无状态、易扩展、适合分布式。
- ✅ 可对接 OAuth2、LDAP、RBAC、ABAC 等复杂模型。
📌 如何集成(Spring Boot 3 + Spring Security + JWT)
1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
2. JWT 工具类(JwtUtil.java)
@Component
public class JwtUtil {
private final String SECRET_KEY = "yourSecretKeyShouldBeLongAndRandom"; // 应从配置读取
private final long EXPIRATION = 86400000; // 24小时
public String generateToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
3. Security 配置(SecurityConfig.java)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login").permitAll()
.requestMatchers("/swagger-ui.html", "/v3/api-docs/**").permitAll() // 放行 Swagger
.requestMatchers(HttpMethod.GET, "/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/users").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/users/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4. JWT 过滤器(JwtAuthenticationFilter.java)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String header = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (header != null && header.startsWith("Bearer ")) {
jwt = header.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtUtil.validateToken(jwt, username)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, null, getAuthorities(jwt));
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
private List<SimpleGrantedAuthority> getAuthorities(String token) {
List<String> roles = jwtUtil.extractClaim(token, claims -> claims.get("roles", List.class));
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
}
5. 登录接口示例(AuthController.java)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public Result<String> login(@RequestBody LoginDTO loginDTO) {
// 模拟校验(实际应查数据库)
if ("admin".equals(loginDTO.getUsername()) && "123456".equals(loginDTO.getPassword())) {
String token = jwtUtil.generateToken(loginDTO.getUsername(), Arrays.asList("ADMIN"));
return Result.success(token);
}
return Result.error("用户名或密码错误");
}
}
📌 最佳实践建议:
- ✅ 敏感接口(如删除、修改)必须加权限控制。
- ✅ 使用
@PreAuthorize("hasRole('ADMIN')")方法级注解。 - ✅ JWT 密钥从配置中心或环境变量读取,禁止硬编码。
- ✅ 设置合理的 Token 过期时间,支持 Refresh Token 机制。
- ✅ 记录登录日志、失败次数、IP 限制等安全策略。
✅ 推荐扩展 3:添加操作日志注解 + AOP 切面记录
📌 作用:
- 自动记录用户操作行为(如“张三修改了用户李四的信息”)。
- 用于审计追踪、故障排查、合规审查。
- 解耦业务代码与日志逻辑。
📌 为什么推荐?
- ❌ 传统痛点:日志散落在业务代码中,难以维护、格式不统一、遗漏关键操作。
- ✅ AOP 切面统一处理,业务无侵入。
- ✅ 注解驱动,灵活控制哪些方法需要记录日志。
- ✅ 支持记录操作人、IP、参数、耗时、结果等。
📌 如何集成(自定义注解 + AOP)
1. 自定义注解(@Log.java)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String value() default ""; // 操作描述
String module() default "系统模块"; // 模块名
}
2. 日志实体类(SysLog.java)
@Data
@TableName("sys_log")
public class SysLog {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username; // 操作人
private String operation; // 操作描述
private String method; // 方法名
private String params; // 请求参数(JSON)
private String ip; // IP地址
private Long time; // 耗时(毫秒)
private LocalDateTime createTime;
}
3. AOP 切面(LogAspect.java)
@Aspect
@Component
public class LogAspect {
@Autowired
private ISysLogService logService; // 日志服务
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void logPointCut() {}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = point.proceed();
// 保存日志
saveLog(point, System.currentTimeMillis() - beginTime);
return result;
}
private void saveLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log logAnnotation = method.getAnnotation(Log.class);
SysLog sysLog = new SysLog();
sysLog.setOperation(logAnnotation.value());
sysLog.setMethod(signature.getDeclaringTypeName() + "." + signature.getName());
// 获取请求参数
Object[] args = joinPoint.getArgs();
sysLog.setParams(JSON.toJSONString(args));
// 获取用户名(从 SecurityContext 或 ThreadLocal)
String username = SecurityContextHolder.getContext().getAuthentication() != null ?
SecurityContextHolder.getContext().getAuthentication().getName() : "匿名用户";
sysLog.setUsername(username);
// 获取 IP
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
sysLog.setIp(request.getRemoteAddr());
sysLog.setTime(time);
sysLog.setCreateTime(LocalDateTime.now());
// 异步保存(避免阻塞主流程)
CompletableFuture.runAsync(() -> logService.save(sysLog));
}
}
4. 在 Controller 方法上使用
@Log("创建用户")
@PostMapping
public Result<String> createUser(@Valid @RequestBody UserCreateDTO createDTO) {
// ...
}
@Log("删除用户")
@DeleteMapping("/{id}")
public Result<String> deleteUser(@PathVariable Long id) {
// ...
}
📌 最佳实践建议:
- ✅ 关键业务操作(增删改)必须加
@Log。 - ✅ 日志表做分区或归档,避免单表过大。
- ✅ 使用异步保存(如
@Async或CompletableFuture),避免影响接口性能。 - ✅ 敏感参数(如密码)应在保存前脱敏。
- ✅ 结合 ELK 或 Grafana 做日志可视化分析。
✅ 推荐扩展 4:使用 MapStruct 做 DTO ↔ Entity 转换
📌 作用:
- 在 DTO(数据传输对象)和 Entity(持久化对象)之间高性能、类型安全地转换。
- 避免手动
set/get或使用反射(如 BeanUtils)带来的性能损耗和类型隐患。
📌 为什么推荐?
- ❌ 传统痛点:
- 手动 set/get → 代码冗长、易漏字段、难维护。
- BeanUtils.copyProperties → 性能差、类型不安全、无法定制逻辑。
- ✅ MapStruct 编译期生成转换代码 → 零反射、高性能、类型安全。
- ✅ 支持复杂映射、默认值、表达式、嵌套对象等。
- ✅ 大型项目必备,提升代码健壮性和可维护性。
📌 如何集成(MapStruct + Lombok)
1. 添加依赖
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 配置编译插件(让 Lombok 和 MapStruct 兼容)
<!-- 以下内容参考自 MapStruct 官方 https://github.com/mapstruct/mapstruct-examples/blob/main/mapstruct-lombok/pom.xml-->
<!-- 中文文档 https://www.mapstruct.plus/mapstruct/1-5-5-Final.html -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
3. 定义映射器(UserMapper.java —— 注意不是 MyBatis Mapper)
// 注意:添加 org.mapstruct.Mapper 注解
@Mapper // @Mapper 注解将接口标记为映射接口,并允许 MapStruct 处理器在编译期间启动。
public interface UserStructMapper {
// 实际的映射方法,期望以源对象作为参数,并返回目标对象。其名称可以自由选择。
UserStructMapper INSTANCE = Mappers.getMapper(UserStructMapper.class);
// 对于源对象和目标对象中具有不同名称的属性,可以使用 @Mapping 注解来配置名称。
// DTO → Entity
@Mapping(target = "password", ignore = true)
@Mapping(target = "createTime", ignore = true) // 忽略自动填充字段
@Mapping(target = "updateTime", ignore = true)
@Mapping(source = "numberOfSeats", target = "seatCount")
User toEntity(UserCreateDTO dto);
// Entity → VO(查询返回用)
UserVO toVO(User entity);
// 批量转换
List<UserVO> toVOList(List<User> entities);
}
// MapStruct 官方示例 https://github.com/mapstruct/mapstruct-examples
4. 在 Service 中使用
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserStructMapper userStructMapper;
@Override
public boolean createUser(UserCreateDTO dto) {
// 自动转换
User user = userStructMapper.INSTANCE.toEntity( dto );
return this.save(user);
}
@Override
public UserVO getUserById(Long id) {
User user = this.getById(id);
return user != null ? userStructMapper.INSTANCE.toVO(user) : null;
}
@Override
public IPage<UserVO> searchUsers(UserQueryDTO query, Page<User> page) {
IPage<User> userPage = this.page(page, buildWrapper(query));
// 转换分页数据
List<UserVO> voList = userStructMapper.INSTANCE.toVOList(userPage.getRecords());
IPage<UserVO> voPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal());
voPage.setRecords(voList);
return voPage;
}
}
5. 定义 UserVO(返回给前端的对象)
@Data
public class UserVO {
private Long id;
private String name;
private Integer age;
private String email;
private String statusDesc; // 转换后的状态描述(如“启用”)
private String createTime; // 格式化后的时间
}
并扩展映射器:
@Mapper
public interface UserStructMapper {
@Mapping(target = "statusDesc", expression = "java(getStatusDesc(entity.getStatus()))")
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
UserVO toVO(User entity);
default String getStatusDesc(Integer status) {
return status == 1 ? "启用" : "禁用";
}
}
📌 最佳实践建议:
- ✅ 所有对外接口的入参用 DTO,出参用 VO,Entity 仅用于持久层。
- ✅ 不要让 Entity 流出 Service 层(避免暴露数据库字段或敏感信息)。
- ✅ 复杂转换逻辑用
default方法或@AfterMapping。 - ✅ 使用
@Mapper(componentModel = "spring")让 Spring 管理实例。 - ✅ IDEA 安装 MapStruct 插件,支持跳转和错误提示。
✅ 总结:四大扩展的价值矩阵
| 扩展项 | 解决痛点 | 提升方向 | 适用阶段 |
|---|---|---|---|
| Swagger | 文档不同步、难调试 | 协作效率、开发体验 | 项目初期必选 |
| Spring Security + JWT | 权限裸奔、越权风险 | 系统安全、合规性 | 有用户系统必选 |
| AOP 操作日志 | 审计无记录、排查困难 | 可追溯性、运维能力 | 中大型项目推荐 |
| MapStruct | 手动转换易错、性能差 | 代码质量、性能、安全 | 大型项目必备 |
📌 最终建议架构图:
前端 → [Swagger UI] → Controller (@Log + @PreAuthorize)
↓
DTO ←→ [MapStruct] ←→ Entity
↓
Service → [MyBatis-Plus] → Mapper → Database
↓
[AOP 日志] → SysLog 表
↓
[Security Context] ← JWT ← Auth Filter
通过以上四大扩展的集成,你的 Spring Boot 项目将具备:
✅ 专业级 API 文档
✅ 企业级安全防护
✅ 完善的操作审计
✅ 高性能类型安全转换
这不仅是“能用”的系统,更是“好用、安全、可维护、可扩展”的企业级生产系统!
💡 温馨提示:可根据项目规模和团队能力逐步引入,不必一步到位。先上 Swagger 和 Security,再逐步加入日志和 MapStruct。

348

被折叠的 条评论
为什么被折叠?



