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. 接口幂等:防重复提交的常见方案
典型场景:下单、支付回调、表单提交按钮连点。
常见方案:
- 前端生成幂等 token ,后端用 Redis
SETNX校验 - 业务唯一键(订单号、请求号)+ 数据库唯一索引(最可靠)
- 分布式锁(粒度较大,慎用)
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)