Java 开发实战:从分层架构到性能优化(Spring Boot + MyBatis-Plus + Redis + JWT)

Java 开发实战:从分层架构到性能优化(Spring Boot + MyBatis-Plus + Redis + JWT)

适合人群:有一定 Java 基础,想把"能写代码"提升到"能写工程化代码"的同学

关键词:分层架构、统一返回、全局异常、参数校验、登录鉴权、分页与查询、事务、缓存、幂等、性能优化、日志与监控


1. 为什么要"工程化"写 Java?

很多同学写 Java 的常见路径是:Controller 里直接写业务 → 调 Service → 调 Mapper → 结束。短期能跑,但长期会出现:

  • 业务逻辑散落在 Controller,难以维护
  • 返回值不统一,前端对接成本高
  • 异常处理不集中,定位困难
  • 参数校验靠手写 if,重复且容易漏
  • 性能问题(N+1、慢 SQL、缓存击穿)出现后无从下手

这篇文章用一套"可直接落地"的实践方案,把常见工程问题一次讲透,并给出可复制的代码结构。


2. 推荐的项目分层与目录结构

经典分层(可应对 90% 企业项目):

  • controller:只负责接收参数、调用 service、返回结果
  • service:业务编排(事务、组合调用、领域规则)
  • mapper/dao:数据库访问(MyBatis/MyBatis-Plus)
  • entity/po:数据库对象
  • dto/vo:入参/出参对象(与 entity 分离)
  • config:配置类(WebMvc、Redis、MyBatis、Jackson)
  • common:通用工具(统一返回、异常、枚举、常量)
  • security/auth:鉴权、JWT、拦截器等
  • infra:基础设施(消息队列、第三方接口、对象存储)

示例结构:

text 复制代码
src/main/java/com/example/demo
├── common
│   ├── api (Result, ResultCode)
│   ├── exception (BizException, GlobalExceptionHandler)
│   └── util (IdUtil, JsonUtil)
├── config (RedisConfig, WebConfig, MybatisConfig)
├── controller (UserController)
├── dto (UserCreateDTO, LoginDTO)
├── entity (User)
├── mapper (UserMapper)
├── service (UserService, impl)
├── vo (UserVO, TokenVO)
└── security (JwtUtil, AuthInterceptor)

3. 统一返回体:让前后端对接更丝滑

统一返回体的好处:

  • 前端永远按 code/msg/data 解析
  • 便于全局异常统一处理
  • 便于网关/监控/日志采集

3.1 Result 结构

java 复制代码
public class Result<T> {
    private int code;
    private String msg;
    private T data;
    private long ts;

    public static <T> Result<T> ok(T data) {
        Result<T> r = new Result<>();
        r.code = 0;
        r.msg = "success";
        r.data = data;
        r.ts = System.currentTimeMillis();
        return r;
    }

    public static <T> Result<T> fail(int code, String msg) {
        Result<T> r = new Result<>();
        r.code = code;
        r.msg = msg;
        r.ts = System.currentTimeMillis();
        return r;
    }
}

3.2 状态码枚举

java 复制代码
public enum ResultCode {
    PARAM_ERROR(40001, "参数错误"),
    UNAUTHORIZED(40100, "未登录或登录已过期"),
    FORBIDDEN(40300, "无权限"),
    BIZ_ERROR(50001, "业务异常"),
    SYSTEM_ERROR(50000, "系统异常");

    public final int code;
    public final String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

4. 全局异常处理:把 try-catch 从业务中赶出去

4.1 自定义业务异常 BizException

java 复制代码
public class BizException extends RuntimeException {
    private final int code;

    public BizException(ResultCode rc) {
        super(rc.msg);
        this.code = rc.code;
    }

    public BizException(int code, String msg) {
        super(msg);
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

4.2 全局异常处理器

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BizException.class)
    public Result<Void> handleBiz(BizException e) {
        return Result.fail(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValid(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors()
                .stream()
                .findFirst()
                .map(err -> err.getField() + " " + err.getDefaultMessage())
                .orElse("参数校验失败");
        return Result.fail(ResultCode.PARAM_ERROR.code, msg);
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleOther(Exception e) {
        // 生产中记得打印完整堆栈 + traceId
        return Result.fail(ResultCode.SYSTEM_ERROR.code, "系统繁忙,请稍后重试");
    }
}

5. 参数校验:用注解替代手写 if

引入依赖(Spring Boot 3.x 一般自带;旧版本需要 spring-boot-starter-validation):

xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

DTO 示例:

java 复制代码
public class UserCreateDTO {
    @NotBlank(message = "不能为空")
    private String username;

    @NotBlank(message = "不能为空")
    private String password;

    @Email(message = "格式不正确")
    private String email;
}

Controller:

java 复制代码
@PostMapping("/users")
public Result<Long> create(@RequestBody @Valid UserCreateDTO dto) {
    Long id = userService.create(dto);
    return Result.ok(id);
}

6. 登录鉴权:JWT + 拦截器(简洁通用版)

企业里常用:JWT(无状态)或 Session(有状态),也可能用 Spring Security。这里先给一个"轻量可控"的 JWT 方案。

6.1 JWT 工具类(示意)

java 复制代码
public class JwtUtil {
    private static final String SECRET = "replace_to_your_secret";
    private static final long EXPIRE_MS = 2 * 60 * 60 * 1000L;

    public static String generateToken(Long userId) {
        long now = System.currentTimeMillis();
        Date exp = new Date(now + EXPIRE_MS);

        return Jwts.builder()
                .setSubject(String.valueOf(userId))
                .setIssuedAt(new Date(now))
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
    }

    public static Long parseUserId(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
        return Long.valueOf(claims.getSubject());
    }
}

6.2 拦截器校验 token

java 复制代码
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        if (token == null || token.isBlank()) {
            throw new BizException(ResultCode.UNAUTHORIZED);
        }
        try {
            Long userId = JwtUtil.parseUserId(token.replace("Bearer ", ""));
            request.setAttribute("userId", userId);
            return true;
        } catch (Exception e) {
            throw new BizException(ResultCode.UNAUTHORIZED.code, "token无效或已过期");
        }
    }
}

6.3 WebMvc 注册拦截器

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login", "/api/register");
    }
}

7. 数据库访问:MyBatis-Plus 常用写法与踩坑

7.1 Entity 与字段规范

建议:

  • 主键统一 id
  • 时间统一 createTime / updateTime
  • 逻辑删除 deleted
  • 建议配合自动填充(MP 的 MetaObjectHandler
java 复制代码
@TableName("t_user")
public class User {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String username;
    private String password;
    private String email;

    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

7.2 常见查询

java 复制代码
// 按用户名查
User user = userMapper.selectOne(
    new LambdaQueryWrapper<User>()
        .eq(User::getUsername, username)
        .last("limit 1")
);

// 分页
Page<User> page = new Page<>(current, size);
Page<User> res = userMapper.selectPage(page,
    new LambdaQueryWrapper<User>().like(User::getUsername, keyword)
);

7.3 避坑:N+1 与批量查询

反例(N+1):

java 复制代码
List<Order> orders = orderMapper.selectList(...);
for (Order o : orders) {
    User u = userMapper.selectById(o.getUserId()); // N 次
}

优化:

  • in 批量查 user
  • 或一条 join SQL(更推荐)

8. 事务:什么时候用 @Transactional?

经验规则:

  • 同一业务链路中涉及多次写操作(insert/update/delete)→ 必须事务
  • 对外部系统调用(MQ、HTTP)→ 谨慎放进事务(可能导致事务时间过长)
  • 事务默认只对 RuntimeException 回滚(也可以配置)
java 复制代码
@Transactional(rollbackFor = Exception.class)
public Long create(UserCreateDTO dto) {
    // 1. 校验唯一性
    if (existsByUsername(dto.getUsername())) {
        throw new BizException(ResultCode.BIZ_ERROR.code, "用户名已存在");
    }

    // 2. 写入
    User user = new User();
    user.setUsername(dto.getUsername());
    user.setPassword(hash(dto.getPassword()));
    user.setEmail(dto.getEmail());
    userMapper.insert(user);

    // 3. 其他关联表写入...
    return user.getId();
}

9. Redis 缓存:缓存策略 + 三个经典问题

9.1 缓存策略怎么选?

  • 旁路缓存(Cache Aside) :最常用
    • 读:先查缓存,miss 再查 DB,写入缓存
    • 写:先写 DB,再删缓存(或更新缓存)
  • 读写穿透:更像框架封装

一般业务建议 旁路缓存

9.2 缓存穿透、击穿、雪崩

  • 穿透 :恶意请求不存在的 key → DB 被打爆
    解决:缓存空值 + 布隆过滤器 + 参数校验/限流
  • 击穿 :热点 key 过期瞬间大量并发 miss
    解决:互斥锁/单飞(singleflight)+ 热点永不过期(后台刷新)
  • 雪崩 :大量 key 同时过期
    解决:过期时间加随机、分批预热、限流熔断

9.3 示例:查询用户信息

java 复制代码
public UserVO getUser(Long id) {
    String key = "user:" + id;
    String cache = redisTemplate.opsForValue().get(key);
    if (cache != null) {
        return JsonUtil.fromJson(cache, UserVO.class);
    }

    User user = userMapper.selectById(id);
    if (user == null) {
        // 缓存空值,短 TTL 防穿透
        redisTemplate.opsForValue().set(key, "", 60, TimeUnit.SECONDS);
        throw new BizException(ResultCode.BIZ_ERROR.code, "用户不存在");
    }

    UserVO vo = convert(user);
    redisTemplate.opsForValue().set(key, JsonUtil.toJson(vo), 5, TimeUnit.MINUTES);
    return vo;
}

10. 接口幂等:防重复提交的常见方案

典型场景:下单、支付回调、表单提交按钮连点。

常见方案:

  1. 前端生成幂等 token ,后端用 Redis SETNX 校验
  2. 业务唯一键(订单号、请求号)+ 数据库唯一索引(最可靠)
  3. 分布式锁(粒度较大,慎用)

Redis SETNX 示例:

java 复制代码
public void submitOrder(String reqId) {
    String key = "idem:" + reqId;
    Boolean ok = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", 2, TimeUnit.MINUTES);
    if (Boolean.FALSE.equals(ok)) {
        throw new BizException(ResultCode.BIZ_ERROR.code, "请勿重复提交");
    }
    // 正常业务...
}

11. 性能优化:从 SQL 到 JVM 的排查路线

11.1 SQL 优化优先级最高

  • 先看执行计划:是否走索引、是否全表扫描
  • 索引设计原则:
    • 等值查询列(=)优先
    • 选择区分度高的列
    • 联合索引遵循最左前缀
  • 避免:
    • like '%xxx'(前置通配)
    • where function(col)=... 破坏索引
    • 大表 order by + limit 不走索引

11.2 代码层面常见问题

  • stream().filter() 在大列表上做复杂逻辑(可用 Map/Set 加速)
  • 频繁 JSON 序列化/反序列化
  • 大对象在循环中重复创建
  • 日志打印过多(尤其是 debug + 大对象)

11.3 JVM/线上排查(最常用三件套)

  • jps:找进程
  • jstack:看线程是否死锁/阻塞
  • jmap/jcmd:看堆、dump 分析内存泄漏

生产中建议配合:

  • APM:SkyWalking / Pinpoint
  • 日志:ELK / Loki
  • 指标:Prometheus + Grafana

12. 日志规范:让问题"可追踪、可复现"

关键点:

  • 每次请求都有 traceId(网关/服务生成)
  • 日志分级:debug/info/warn/error
  • error 必须打印堆栈
  • 关键业务埋点:下单、支付、退款等

简化版:用 MDC 放 traceId(示意)

java 复制代码
MDC.put("traceId", UUID.randomUUID().toString().replace("-", ""));
log.info("create user, username={}", dto.getUsername());

13. 一个完整的小例子:注册 + 登录 + 查询用户

13.1 Controller

java 复制代码
@RestController
@RequestMapping("/api")
public class UserController {

    @PostMapping("/register")
    public Result<Long> register(@RequestBody @Valid UserCreateDTO dto) {
        return Result.ok(userService.create(dto));
    }

    @PostMapping("/login")
    public Result<TokenVO> login(@RequestBody @Valid LoginDTO dto) {
        return Result.ok(userService.login(dto));
    }

    @GetMapping("/users/{id}")
    public Result<UserVO> detail(@PathVariable Long id) {
        return Result.ok(userService.getUser(id));
    }
}

13.2 Service

java 复制代码
@Service
public class UserService {

    @Resource private UserMapper userMapper;

    public Long create(UserCreateDTO dto) {
        User exist = userMapper.selectOne(new LambdaQueryWrapper<User>()
                .eq(User::getUsername, dto.getUsername())
                .last("limit 1"));
        if (exist != null) {
            throw new BizException(ResultCode.BIZ_ERROR.code, "用户名已存在");
        }

        User u = new User();
        u.setUsername(dto.getUsername());
        u.setPassword(hash(dto.getPassword()));
        u.setEmail(dto.getEmail());
        userMapper.insert(u);
        return u.getId();
    }

    public TokenVO login(LoginDTO dto) {
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                .eq(User::getUsername, dto.getUsername())
                .last("limit 1"));
        if (user == null || !verify(dto.getPassword(), user.getPassword())) {
            throw new BizException(ResultCode.BIZ_ERROR.code, "账号或密码错误");
        }
        String token = JwtUtil.generateToken(user.getId());
        return new TokenVO(token);
    }

    public UserVO getUser(Long id) {
        User user = userMapper.selectById(id);
        if (user == null) throw new BizException(ResultCode.BIZ_ERROR.code, "用户不存在");
        return convert(user);
    }

    private String hash(String raw) { return raw; } // 这里示意,生产请用 BCrypt
    private boolean verify(String raw, String hashed) { return raw.equals(hashed); }
    private UserVO convert(User u) { return new UserVO(u.getId(), u.getUsername(), u.getEmail()); }
}

14. 总结:一套可复用的 Java 项目"底座"

把这套底座搭好,你会发现:

  • 需求改动更容易:业务在 service,controller 更干净
  • 线上更稳定:异常统一、日志可追踪
  • 性能更可控:缓存、批量查询、SQL 优化有抓手
  • 团队协作更高效:规范统一、代码可读性强

如果你希望我把本文再升级成"企业级模板",我也可以继续补全:

  • Spring Security 版本的鉴权(RBAC 权限模型)
  • 统一响应 + Jackson 时间格式 + 枚举序列化
  • MyBatis-Plus 自动填充、逻辑删除、乐观锁
  • Redis 分布式锁(Redisson)与热点缓存方案
  • 分库分表/读写分离的落地思路
  • 一套可运行的 GitHub 项目骨架(含 docker-compose)
相关推荐
聆风吟º3 小时前
【Spring Boot 报错已解决】Spring Boot项目启动报错 “Main method not found“ 的全面分析与解决方案
android·spring boot·后端
spencer_tseng3 小时前
RedisConnectionMonitor.java
java
Rover.x3 小时前
Arthas内存泄露排查
java·后端
艺杯羹3 小时前
掌握Spring Boot配置艺术:从YAML基础到实战进阶
java·spring boot·后端·yaml
loosed3 小时前
ubuntu navicat17连接本机msyql8 /run/mysqld/mysqld.sock问题
linux·mysql·ubuntu·adb
Lin_Miao_093 小时前
基于 DataX + DataX-Web 生成报表数据
java·数据库
沉迷技术逻辑3 小时前
微服务架构-网关
java·微服务·架构
一位代码3 小时前
mysql | 复制表结构和数据
数据库·mysql