初入职场用Spring MVC写接口总500报错?从环境配置到微服务架构,深度解析企业级开发避坑指南与实战项目
写在前面
刚入职场那会儿,我遇到最多的就是500错误。前端同学风风火火跑来问:”接口报500了,快看看!” 我盯着满屏红彤彤的堆栈信息,心里慌得一批。今天我就把自己踩过的坑、总结的经验,毫无保留地分享给你。
一、500错误到底是什么鬼?
首先得搞清楚,500错误是服务器内部错误。这意味着你的代码在服务器上跑着跑着,突然炸了。
常见的几种原因:
- 空指针异常(NullPointerException)
- 数据库连接失败
- 依赖注入失败
- 配置文件写错了
- 事务回滚失败
- 第三方接口调用超时
二、环境配置阶段最容易踩的坑
2.1 项目结构搭建
一个标准的Spring MVC项目结构应该是这样的:
project-root/
├── pom.xml # Maven依赖配置
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/yourcompany/
│ │ │ └── demo/
│ │ │ ├── DemoApplication.java # 启动类
│ │ │ ├── config/ # 配置类
│ │ │ ├── controller/ # 控制器
│ │ │ ├── service/ # 业务层
│ │ │ ├── dao/ # 数据访问层
│ │ │ ├── entity/ # 实体类
│ │ │ ├── dto/ # 数据传输对象
│ │ │ ├── vo/ # 视图对象
│ │ │ ├── enums/ # 枚举类
│ │ │ ├── exception/ # 异常处理
│ │ │ └── utils/ # 工具类
│ │ └── resources/
│ │ ├── application.yml # 主配置文件
│ │ ├── application-dev.yml # 开发环境配置
│ │ ├── application-prod.yml # 生产环境配置
│ │ └── mapper/ # MyBatis XML映射文件
│ └── test/
│ └── java/ # 测试代码
└── .gitignore
2.2 pom.xml依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 继承Spring Boot父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>demo</name>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
<druid.version>1.2.21</druid.version>
<fastjson2.version>2.0.43</fastjson2.version>
<swagger.version>3.0.0</swagger.version>
<redis.version>3.2.0</redis.version>
<jwt.version>0.12.3</jwt.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot Actuator 健康检查 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Swagger/OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.3 application.yml 配置
# ==================== 主配置 ====================
spring:
application:
name: demo-service
# 多环境配置
profiles:
active: dev
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: your_password
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 监控配置
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
# Redis配置
data:
redis:
host: localhost
port: 6379
password: ""
database: 0
timeout: 5000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# Jackson配置
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
# 文件上传配置
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
# MVC配置
mvc:
throw-exception-if-no-handler-found: true
# 禁用默认的错误页面(使用自定义异常处理)
web:
resources:
add-mappings: false
# ==================== MyBatis Plus配置 ====================
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.demo.entity
global-config:
db-config:
id-type: ASSIGN_ID # 主键策略:雪花算法
logic-delete-field: deleted # 全局逻辑删除字段
logic-delete-value: 1
logic-not-delete-value: 0
table-underline: true # 表名下划线命名
configuration:
map-underscore-to-camel-case: true # 驼峰转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL到控制台(开发环境)
cache-enabled: false
# ==================== 日志配置 ====================
logging:
level:
root: INFO
com.example.demo: DEBUG
org.springframework: INFO
org.mybatis: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
file:
name: logs/demo-service.log
max-size: 10MB
max-history: 30
total-size-cap: 1GB
# ==================== 服务器配置 ====================
server:
port: 8080
servlet:
context-path: /api
tomcat:
uri-encoding: UTF-8
max-threads: 200
min-spare-threads: 10
# ==================== Swagger/OpenAPI配置 ====================
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
三、500错误常见原因及解决方案
3.1 空指针异常(最常见!)
// ❌ 错误写法:忘记判空
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Result getUser(@PathVariable Long id) {
// 如果id不存在,user可能为null,后面直接调用user.getName()就会500
User user = userService.getById(id);
return Result.success(user.getName()); // 可能NPE!
}
}
// ✅ 正确写法:做好判空处理
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Result getUser(@PathVariable Long id) {
User user = userService.getById(id);
if (user == null) {
return Result.error(404, "用户不存在");
}
return Result.success(user.getName());
}
}
3.2 数据库连接问题
// 数据库连接失败是500的大户
// 检查以下几点:
// 1. 数据库是否启动
// 2. 连接URL是否正确(注意时区配置)
// 3. 用户名密码是否正确
// 4. 防火墙是否放行3306端口
// application-dev.yml 中建议添加以下调试配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=30000
3.3 依赖注入失败
// ❌ 错误:注入的Bean不存在或名称不匹配
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 如果UserMapper没有加@Mapper注解,就会注入失败
public User getById(Long id) {
return userMapper.selectById(id);
}
}
// ✅ 方案一:在Mapper接口上加@Mapper注解
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
// ✅ 方案二:在启动类上加@MapperScan
@SpringBootApplication
@MapperScan("com.example.demo.dao")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
// ✅ 方案三:使用required=false允许注入失败时不报错
@Service
public class UserService {
@Autowired(required = false)
private UserMapper userMapper;
public User getById(Long id) {
if (userMapper == null) {
throw new RuntimeException("UserMapper未注入");
}
return userMapper.selectById(id);
}
}
3.4 全局异常处理
这是解决500问题的核心武器:
/**
* 统一异常处理类
* 这是避免500错误裸露给用户的关键!
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.findFirst()
.orElse("参数校验失败");
log.warn("参数校验失败: {}", message);
return Result.error(400, message);
}
/**
* 参数绑定异常
*/
@ExceptionHandler(BindException.class)
public Result handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.findFirst()
.orElse("参数绑定失败");
log.warn("参数绑定失败: {}", message);
return Result.error(400, message);
}
/**
* 空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public Result handleNullPointerException(NullPointerException e) {
log.error("空指针异常: ", e);
return Result.error(500, "服务器内部错误,请联系管理员");
}
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 数据库异常
*/
@ExceptionHandler(DataAccessException.class)
public Result handleDataAccessException(DataAccessException e) {
log.error("数据库操作异常: ", e);
return Result.error(500, "数据库操作失败,请稍后重试");
}
/**
* 权限不足异常
*/
@ExceptionHandler(AccessDeniedException.class)
public Result handleAccessDeniedException(AccessDeniedException e) {
log.warn("权限不足: ", e);
return Result.error(403, "权限不足,无法执行此操作");
}
/**
* 通用异常
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("系统异常: ", e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
/**
* 业务异常类
* 用于抛出业务逻辑相关的异常,避免返回500
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BusinessException extends RuntimeException {
private Integer code;
private String message;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
}
/**
* 结果码枚举
*/
@Getter
@AllArgsConstructor
public enum ResultCode {
SUCCESS(200, "操作成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "权限不足"),
NOT_FOUND(404, "资源不存在"),
METHOD_NOT_ALLOWED(405, "请求方法不允许"),
CONFLICT(409, "资源冲突"),
TOO_MANY_REQUESTS(429, "请求过于频繁"),
INTERNAL_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务不可用"),
// 自定义业务状态码
USER_NOT_EXIST(1001, "用户不存在"),
USER_ALREADY_EXIST(1002, "用户已存在"),
PASSWORD_ERROR(1003, "密码错误"),
TOKEN_EXPIRED(1004, "Token已过期"),
TOKEN_INVALID(1005, "Token无效"),
ACCOUNT_LOCKED(1006, "账号已被锁定");
private final Integer code;
private final String message;
}
/**
* 统一返回结果类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
private String traceId;
public static <T> Result<T> success(T data) {
return Result.<T>builder()
.code(ResultCode.SUCCESS.getCode())
.message(ResultCode.SUCCESS.getMessage())
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(Integer code, String message) {
return Result.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> Result<T> error(ResultCode resultCode) {
return error(resultCode.getCode(), resultCode.getMessage());
}
public static <T> Result<T> error(ResultCode resultCode, String message) {
return Result.<T>builder()
.code(resultCode.getCode())
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
public boolean isSuccess() {
return ResultCode.SUCCESS.getCode().equals(this.code);
}
}
四、实战项目:用户管理系统
4.1 实体类
/**
* 用户实体类
* 使用Lombok简化代码
*/
@Data
@TableName("sys_user")
public class User {
/** 主键ID */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 用户名 */
private String username;
/** 密码(加密存储) */
private String password;
/** 真实姓名 */
private String realName;
/** 手机号 */
private String phone;
/** 邮箱 */
private String email;
/** 性别:0-未知 1-男 2-女 */
private Integer gender;
/** 头像URL */
private String avatar;
/** 状态:0-禁用 1-正常 */
private Integer status;
/** 逻辑删除:0-未删除 1-已删除 */
@TableLogic
private Integer deleted;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/** 创建人 */
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/** 更新人 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
}
4.2 Mapper层
/**
* 用户数据访问层
* 继承MyBatis Plus的BaseMapper,获得常用CRUD方法
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义查询:根据用户名查询用户
*/
@Select("SELECT * FROM sys_user WHERE username = #{username} AND deleted = 0")
User selectByUsername(@Param("username") String username);
/**
* 自定义查询:分页查询用户列表
*/
List<User> selectUserPage(@Param("page") Integer page,
@Param("size") Integer size,
@Param("keyword") String keyword);
}
4.3 Service层
/**
* 用户服务层接口
*/
public interface UserService {
/**
* 根据ID查询用户
*/
User getById(Long id);
/**
* 根据用户名查询用户
*/
User getByUsername(String username);
/**
* 查询用户列表(分页)
*/
PageResult<User> listUser(Integer page, Integer size, String keyword);
/**
* 新增用户
*/
void addUser(UserDTO userDTO);
/**
* 更新用户
*/
void updateUser(UserDTO userDTO);
/**
* 删除用户
*/
void deleteUser(Long id);
/**
* 批量删除用户
*/
void batchDeleteUsers(List<Long> ids);
/**
* 修改用户状态
*/
void updateUserStatus(Long id, Integer status);
/**
* 用户登录
*/
LoginResult login(LoginDTO loginDTO);
/**
* 修改密码
*/
void updatePassword(Long userId, UpdatePasswordDTO dto);
}
/**
* 用户服务层实现
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokenService tokenService;
@Override
public User getById(Long id) {
// 关键:先检查id是否为空
if (id == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户ID不能为空");
}
User user = userMapper.selectById(id);
// 关键:查询结果判空
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_EXIST, "用户不存在");
}
// 密码不返回给前端
user.setPassword(null);
return user;
}
@Override
public User getByUsername(String username) {
if (username == null || username.trim().isEmpty()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户名不能为空");
}
return userMapper.selectByUsername(username.trim());
}
@Override
public PageResult<User> listUser(Integer page, Integer size, String keyword) {
// 参数校验
if (page == null || page < 1) {
page = 1;
}
if (size == null || size < 1 || size > 100) {
size = 10;
}
// 分页查询
Page<User> pageParam = new Page<>(page, size);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.hasText(keyword)) {
queryWrapper.like("username", keyword)
.or()
.like("real_name", keyword)
.or()
.like("phone", keyword);
}
queryWrapper.orderByDesc("create_time");
IPage<User> result = userMapper.selectPage(pageParam, queryWrapper);
// 清除密码
result.getRecords().forEach(user -> user.setPassword(null));
return PageResult.of(result);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void addUser(UserDTO userDTO) {
// 1. 参数校验
validateUserDTO(userDTO, true);
// 2. 检查用户名是否已存在
User existUser = userMapper.selectByUsername(userDTO.getUsername());
if (existUser != null) {
throw new BusinessException(ResultCode.USER_ALREADY_EXIST, "用户名已存在");
}
// 3. 构建用户实体
User user = new User();
user.setUsername(userDTO.getUsername());
// 密码加密存储
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
user.setRealName(userDTO.getRealName());
user.setPhone(userDTO.getPhone());
user.setEmail(userDTO.getEmail());
user.setGender(userDTO.getGender());
user.setStatus(1);
// 4. 插入数据库
int rows = userMapper.insert(user);
if (rows <= 0) {
throw new BusinessException(ResultCode.INTERNAL_ERROR, "用户创建失败");
}
log.info("用户创建成功, userId={}, username={}", user.getId(), user.getUsername());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserDTO userDTO) {
// 1. 参数校验
validateUserDTO(userDTO, false);
// 2. 检查用户是否存在
User existUser = userMapper.selectById(userDTO.getId());
if (existUser == null) {
throw new BusinessException(ResultCode.USER_NOT_EXIST, "用户不存在");
}
// 3. 检查用户名是否重复(排除自己)
User sameUsernameUser = userMapper.selectByUsername(userDTO.getUsername());
if (sameUsernameUser != null && !sameUsernameUser.getId().equals(userDTO.getId())) {
throw new BusinessException(ResultCode.USER_ALREADY_EXIST, "用户名已存在");
}
// 4. 更新用户信息
User user = new User();
user.setId(userDTO.getId());
user.setRealName(userDTO.getRealName());
user.setPhone(userDTO.getPhone());
user.setEmail(userDTO.getEmail());
user.setGender(userDTO.getGender());
// 如果传了密码,则更新密码
if (StringUtils.hasText(userDTO.getPassword())) {
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
}
int rows = userMapper.updateById(user);
if (rows <= 0) {
throw new BusinessException(ResultCode.INTERNAL_ERROR, "用户更新失败");
}
log.info("用户更新成功, userId={}", userDTO.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteUser(Long id) {
if (id == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户ID不能为空");
}
int rows = userMapper.deleteById(id);
if (rows <= 0) {
throw new BusinessException(ResultCode.USER_NOT_EXIST, "用户不存在或已被删除");
}
log.info("用户删除成功, userId={}", id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void batchDeleteUsers(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
throw new BusinessException(ResultCode.BAD_REQUEST, "请选择要删除的用户");
}
int rows = userMapper.deleteBatchIds(ids);
log.info("批量删除用户成功, 删除数量={}", rows);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserStatus(Long id, Integer status) {
if (id == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户ID不能为空");
}
if (status == null || (status != 0 && status != 1)) {
throw new BusinessException(ResultCode.BAD_REQUEST, "状态参数错误");
}
User existUser = userMapper.selectById(id);
if (existUser == null) {
throw new BusinessException(ResultCode.USER_NOT_EXIST, "用户不存在");
}
User user = new User();
user.setId(id);
user.setStatus(status);
int rows = userMapper.updateById(user);
if (rows <= 0) {
throw new BusinessException(ResultCode.INTERNAL_ERROR, "状态更新失败");
}
log.info("用户状态更新成功, userId={}, status={}", id, status);
}
@Override
public LoginResult login(LoginDTO loginDTO) {
// 1. 参数校验
if (!StringUtils.hasText(loginDTO.getUsername())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户名不能为空");
}
if (!StringUtils.hasText(loginDTO.getPassword())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "密码不能为空");
}
// 2. 查询用户
User user = userMapper.selectByUsername(loginDTO.getUsername());
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_EXIST, "用户名或密码错误");
}
// 3. 检查账号状态
if (user.getStatus() == 0) {
throw new BusinessException(ResultCode.ACCOUNT_LOCKED, "账号已被锁定,请联系管理员");
}
// 4. 验证密码
if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
throw new BusinessException(ResultCode.PASSWORD_ERROR, "用户名或密码错误");
}
// 5. 生成Token
String token = tokenService.generateToken(user);
log.info("用户登录成功, userId={}, username={}", user.getId(), user.getUsername());
// 6. 返回结果
LoginResult result = new LoginResult();
result.setToken(token);
result.setUserId(user.getId());
result.setUsername(user.getUsername());
result.setRealName(user.getRealName());
result.setAvatar(user.getAvatar());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePassword(Long userId, UpdatePasswordDTO dto) {
if (userId == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户ID不能为空");
}
// 1. 查询用户
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_EXIST, "用户不存在");
}
// 2. 验证旧密码
if (!passwordEncoder.matches(dto.getOldPassword(), user.getPassword())) {
throw new BusinessException(ResultCode.PASSWORD_ERROR, "原密码错误");
}
// 3. 新密码不能和旧密码相同
if (dto.getOldPassword().equals(dto.getNewPassword())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "新密码不能与原密码相同");
}
// 4. 更新密码
User updateUser = new User();
updateUser.setId(userId);
updateUser.setPassword(passwordEncoder.encode(dto.getNewPassword()));
int rows = userMapper.updateById(updateUser);
if (rows <= 0) {
throw new BusinessException(ResultCode.INTERNAL_ERROR, "密码修改失败");
}
log.info("用户修改密码成功, userId={}", userId);
}
/**
* 参数校验
* @param isAdd 是否为新增操作(新增时需要校验密码)
*/
private void validateUserDTO(UserDTO userDTO, boolean isAdd) {
if (userDTO == null) {
throw new BusinessException(ResultCode.BAD_REQUEST, "参数不能为空");
}
if (!StringUtils.hasText(userDTO.getUsername())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户名不能为空");
}
if (userDTO.getUsername().length() < 3 || userDTO.getUsername().length() > 20) {
throw new BusinessException(ResultCode.BAD_REQUEST, "用户名长度应在3-20个字符之间");
}
if (isAdd) {
if (!StringUtils.hasText(userDTO.getPassword())) {
throw new BusinessException(ResultCode.BAD_REQUEST, "密码不能为空");
}
if (userDTO.getPassword().length() < 6 || userDTO.getPassword().length() > 20) {
throw new BusinessException(ResultCode.BAD_REQUEST, "密码长度应在6-20个字符之间");
}
}
if (StringUtils.hasText(userDTO.getPhone())
&& !userDTO.getPhone().matches("^1[3-9]\\d{9}$")) {
throw new BusinessException(ResultCode.BAD_REQUEST, "手机号格式不正确");
}
if (StringUtils.hasText(userDTO.getEmail())
&& !userDTO.getEmail().matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) {
throw new BusinessException(ResultCode.BAD_REQUEST, "邮箱格式不正确");
}
}
}
4.4 Controller层
/**
* 用户控制器
* 注意:所有异常都会被GlobalExceptionHandler捕获
*/
@RestController
@RequestMapping("/user")
@Tag(name = "用户管理", description = "用户相关接口")
public class UserController {
@Autowired
private UserService userService;
/**
* 根据ID查询用户
*/
@GetMapping("/{id}")
@Operation(summary = "根据ID查询用户")
public Result<User> getUser(@PathVariable @Positive Long id) {
// 注意:参数校验注解会触发MethodArgumentNotValidException
// 被GlobalExceptionHandler捕获,返回400而不是500
User user = userService.getById(id);
return Result.success(user);
}
/**
* 查询用户列表(分页)
*/
@GetMapping("/list")
@Operation(summary = "查询用户列表")
public Result<PageResult<User>> listUser(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword) {
return Result.success(userService.listUser(page, size, keyword));
}
/**
* 新增用户
*/
@PostMapping
@Operation(summary = "新增用户")
public Result<Void> addUser(@Valid @RequestBody UserDTO userDTO) {
userService.addUser(userDTO);
return Result.success();
}
/**
* 更新用户
*/
@PutMapping
@Operation(summary = "更新用户")
public Result<Void> updateUser(@Valid @RequestBody UserDTO userDTO) {
userService.updateUser(userDTO);
return Result.success();
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
public Result<Void> deleteUser(@PathVariable @Positive Long id) {
userService.deleteUser(id);
return Result.success();
}
/**
* 批量删除用户
*/
@DeleteMapping("/batch")
@Operation(summary = "批量删除用户")
public Result<Void> batchDeleteUsers(@RequestBody List<Long> ids) {
userService.batchDeleteUsers(ids);
return Result.success();
}
/**
* 修改用户状态
*/
@PutMapping("/{id}/status")
@Operation(summary = "修改用户状态")
public Result<Void> updateUserStatus(@PathVariable Long id,
@RequestParam Integer status) {
userService.updateUserStatus(id, status);
return Result.success();
}
/**
* 用户登录
*/
@PostMapping("/login")
@Operation(summary = "用户登录")
public Result<LoginResult> login(@Valid @RequestBody LoginDTO loginDTO) {
return Result.success(userService.login(loginDTO));
}
/**
* 修改密码
*/
@PutMapping("/password")
@Operation(summary = "修改密码")
public Result<Void> updatePassword(@PathVariable Long id,
@Valid @RequestBody UpdatePasswordDTO dto) {
userService.updatePassword(id, dto);
return Result.success();
}
}
4.5 DTO类
/**
* 用户数据传输对象(用于接收前端参数)
* 使用Bean Validation进行参数校验
*/
@Data
public class UserDTO {
/** 用户ID(更新时必须) */
private Long id;
/** 用户名 */
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度应在3-20个字符之间")
private String username;
/** 密码(新增时需要) */
@Size(min = 6, max = 20, message = "密码长度应在6-20个字符之间")
private String password;
/** 真实姓名 */
@Size(max = 50, message = "真实姓名长度不能超过50个字符")
private String realName;
/** 手机号 */
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/** 邮箱 */
@Email(message = "邮箱格式不正确")
private String email;
/** 性别 */
@Min(value = 0, message = "性别参数错误")
@Max(value = 2, message = "性别参数错误")
private Integer gender;
}
/**
* 登录DTO
*/
@Data
public class LoginDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
/**
* 修改密码DTO
*/
@Data
public class UpdatePasswordDTO {
@NotBlank(message = "原密码不能为空")
private String oldPassword;
@NotBlank(message = "新密码不能为空")
@Size(min = 6, max = 20, message = "新密码长度应在6-20个字符之间")
private String newPassword;
}
4.6 分页结果类
/**
* 分页结果类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {
/** 当前页 */
private Integer page;
/** 每页大小 */
private Integer size;
/** 总记录数 */
private Long total;
/** 总页数 */
private Integer pages;
/** 数据列表 */
private List<T> list;
public static <T> PageResult<T> of(IPage<T> page) {
return PageResult.<T>builder()
.page((int) page.getCurrent())
.size((int) page.getSize())
.total(page.getTotal())
.pages((int) page.getPages())
.list(page.getRecords())
.build();
}
}
五、Spring Security安全配置
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private TokenAuthenticationFilter authenticationFilter;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 禁用CSRF(API项目通常不需要)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态
.authorizeHttpRequests(auth -> auth
// 允许匿名访问的接口
.requestMatchers("/api/user/login").anonymous()
.requestMatchers("/api/user/register").anonymous()
.requestMatchers("/api/public/**").anonymous()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/druid/**").permitAll()
// 其他接口需要认证
.anyRequest().authenticated()
)
.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> {
res.setContentType("application/json;charset=UTF-8");
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
res.getWriter().write(new ObjectMapper()
.writeValueAsString(Result.error(401, "未授权,请先登录")));
})
.accessDeniedHandler((req, res, e) -> {
res.setContentType("application/json;charset=UTF-8");
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
res.getWriter().write(new ObjectMapper()
.writeValueAsString(Result.error(403, "权限不足")));
})
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
六、MyBatis Plus配置
/**
* MyBatis Plus配置类
*/
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 自动填充配置
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 实际项目中应该从SecurityContext获取当前用户ID
this.strictInsertFill(metaObject, "createBy", Long.class, 1L);
this.strictInsertFill(metaObject, "updateBy", Long.class, 1L);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy", Long.class, 1L);
}
};
}
}
七、AOP统一日志处理
/**
* 操作日志AOP
* 记录所有接口调用,方便排查问题
*/
@Aspect
@Component
@Slf4j
public class OperationLogAspect {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.GetMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.PostMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.PutMapping) " +
"|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public Object around(ProceedingJoinPoint point) throws Throwable {
long startTime = System.currentTimeMillis();
// 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String className = point.getTarget().getClass().getName();
String methodName = point.getSignature().getName();
String uri = request.getRequestURI();
String method = request.getMethod();
String params = Arrays.toString(point.getArgs());
String remoteAddr = request.getRemoteAddr();
log.info(">>> 请求开始: uri={}, method={}, params={}, remoteAddr={}",
uri, method, params, remoteAddr);
Object result;
try {
result = point.proceed();
long costTime = System.currentTimeMillis() - startTime;
log.info(">>> 请求成功: uri={}, cost={}ms", uri, costTime);
// 记录操作日志到数据库(异步)
saveOperationLog(className, methodName, uri, method, params, costTime, 1);
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
log.error(">>> 请求失败: uri={}, cost={}ms, error={}",
uri, costTime, e.getMessage(), e);
// 记录操作日志到数据库(异步)
saveOperationLog(className, methodName, uri, method, params, costTime, 0);
throw e;
}
}
private void saveOperationLog(String className, String methodName, String uri,
String method, String params, long costTime, int status) {
// 实际项目中应该异步写入数据库
// OperationLog log = new OperationLog();
// log.setClassName(className);
// log.setMethodName(methodName);
// log.setUri(uri);
// log.setMethod(method);
// log.setParams(params);
// log.setCostTime(costTime);
// log.setStatus(status);
// operationLogService.save(log);
}
}
八、微服务架构下的最佳实践
8.1 服务间调用
/**
* 远程调用配置
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
}
8.2 分布式事务(简化版)
/**
* 分布式事务使用示例
* 注意:实际项目中建议使用Seata等分布式事务框架
*/
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@Autowired
private StockService stockService; // 库存服务
/**
* 创建订单
* 使用本地事务保证一致性
*/
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderDTO dto) {
// 1. 扣减库存(远程调用)
Boolean success = stockService.deductStock(dto.getProductId(), dto.getCount());
if (!success) {
throw new BusinessException(ResultCode.SERVICE_UNAVAILABLE, "库存不足");
}
// 2. 创建订单
Order order = new Order();
order.setProductId(dto.getProductId());
order.setCount(dto.getCount());
order.setUserId(dto.getUserId());
order.setStatus(0); // 待支付
orderMapper.insert(order);
log.info("订单创建成功, orderId={}", order.getId());
return order;
}
}
8.3 消息队列解耦
/**
* RabbitMQ配置
*/
@Configuration
public class RabbitMQConfig {
public static final String ORDER_QUEUE = "order.queue";
public static final String ORDER_EXCHANGE = "order.exchange";
public static final String ORDER_ROUTING_KEY = "order.key";
@Bean
public Queue orderQueue() {
return QueueBuilder.durable(ORDER_QUEUE).build();
}
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(ORDER_EXCHANGE);
}
@Bean
public Binding orderBinding(Queue orderQueue, DirectExchange orderExchange) {
return BindingBuilder.bind(orderQueue)
.to(orderExchange)
.with(ORDER_ROUTING_KEY);
}
}
/**
* 订单服务 - 发送消息
*/
@Service
@Slf4j
public class OrderService {
@Autowired
private AmqpTemplate amqpTemplate;
public void sendOrderMessage(Order order) {
try {
amqpTemplate.convertAndSend(
RabbitMQConfig.ORDER_EXCHANGE,
RabbitMQConfig.ORDER_ROUTING_KEY,
order
);
log.info("订单消息发送成功, orderId={}", order.getId());
} catch (Exception e) {
log.error("订单消息发送失败, orderId={}", order.getId(), e);
// 发送失败可以重试或记录到死信队列
}
}
}
/**
* 库存服务 - 消费消息
*/
@Service
@Slf4j
public class StockConsumer {
@RabbitListener(queues = RabbitMQConfig.ORDER_QUEUE)
public void handleOrderMessage(Order order) {
try {
log.info("开始处理订单消息, orderId={}", order.getId());
// 更新库存状态
// stockService.updateStockStatus(order.getId(), order.getStatus());
log.info("订单消息处理成功, orderId={}", order.getId());
} catch (Exception e) {
log.error("订单消息处理失败, orderId={}", order.getId(), e);
// 可以配置重试机制
throw e; // 抛出异常,消息会重新入队
}
}
}
九、常见500错误排查清单
✅ 启动时检查:
□ 数据库是否启动并能正常连接
□ Redis是否启动
□ 端口是否被占用
□ 配置文件是否正确(特别是密码、URL)
✅ 代码检查:
□ 所有需要判空的地方是否都判空了
□ 数据库查询结果是否判空
□ 参数校验是否完整
□ 事务注解是否正确添加
✅ 日志检查:
□ 查看应用日志(logs/demo-service.log)
□ 查看完整堆栈信息
□ 注意WARN级别的日志
✅ 数据库检查:
□ 表是否存在
□ 字段是否匹配
□ 数据是否符合约束
□ 索引是否合理
✅ 网络检查:
□ 防火墙是否放行
□ 跨域配置是否正确
□ 第三方服务是否可用
十、总结
写接口遇到500错误,不要慌。按照以下思路排查:
- 看日志:日志是最好的朋友,绝大多数问题都能在日志里找到答案
- 查堆栈:定位到具体的行号和类名
- 判空处理:90%的500错误都是因为空指针,养成判空的习惯
- 统一异常处理:配置好GlobalExceptionHandler,别让500裸露给用户
- 参数校验:用Bean Validation,让非法参数在Controller层就拦截
- 数据库检查:确认连接正常,表结构正确
- 单元测试:写测试用例,提前发现问题
记住,好的代码不是不报错,而是报错时能快速定位和修复。希望这份指南能帮你少走弯路,祝你在职场上一路顺风!
