符合企业级开发规范的 Spring Boot + MyBatis-Plus 标准 MVC 结构完整示例

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

以下是一个符合企业级开发规范的 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
  • ✅ 日志表做分区或归档,避免单表过大。
  • ✅ 使用异步保存(如 @AsyncCompletableFuture),避免影响接口性能。
  • ✅ 敏感参数(如密码)应在保存前脱敏。
  • ✅ 结合 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。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值