学完Spring还是不会写代码新手从helloWorld到实际项目遇到的那些坑和解决办法
说实话,刚学完Spring框架那会儿,我特别理解你的感受。看完视频、跟着敲了个HelloWorld,觉得自己行了。结果一打开项目,脑子一片空白,完全不知道从何下手。今天我就把踩过的坑一个个摊开来说,希望能帮你少走弯路。
HelloWorld之后,世界突然变得很陌生
还记得你第一次跑起来的Spring Boot项目吗?一个HelloController,一个main方法,浏览器一访问,”Hello World”出来了。那时候你觉得自己就是程序员了。
然后呢?老师或者教程告诉你,接下来要写一个用户管理系统。你打开IDEA,新建项目,发现——我TM该怎么开始?
这个问题太常见了。HelloWorld只展示了Spring最表层的东西,而真正的项目涉及架构设计、分层、依赖注入、数据库操作、事务管理、异常处理、安全认证等等。从HelloWorld到实际项目,中间隔着的不是一步两步,而是一个完整的工程化思维体系。
让我给你拆解一下,你在这个转型过程中会遇到哪些坑,以及怎么解决。
坑一:依赖注入搞不清楚,@Autowired乱用
刚学完Spring,你知道了IOC容器和依赖注入的概念。老师告诉你,用@Autowired注解就可以自动注入,你很开心,觉得编程可以这么优雅。
然后你在实际项目中,看到各种地方都用@Autowired,你也跟着用,结果问题来了:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderService orderService; // 这里有问题!
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
你可能会发现启动报错:Circular dependency detected(循环依赖)。因为你UserService依赖OrderService,而OrderService又依赖UserService。
怎么解决?
首先要理解依赖注入的本质——控制权反转。你不再是主动创建对象,而是让Spring容器帮你管理。当你需要使用一个Bean的时候,告诉容器你需要什么,容器会自动帮你找到并注入。
对于循环依赖,有几种解法:
第一种,重构设计,从根本上消除循环依赖。循环依赖往往意味着设计有问题:
// 不好:Service之间互相依赖
@Service
public class UserService {
@Autowired
private OrderService orderService;
}
@Service
public class OrderService {
@Autowired
private UserService userService;
}
// 好:引入一个独立的服务来处理共同逻辑
@Service
public class UserService {
@Autowired
private OrderQueryService orderQueryService;
}
@Service
public class OrderService {
@Autowired
private OrderQueryService orderQueryService;
}
第二种,如果短期内无法重构,可以用@Lazy注解延迟加载:
@Service
public class UserService {
@Autowired
@Lazy
private OrderService orderService;
}
第三种,用构造器注入替代字段注入,虽然构造器注入本身也不能解决循环依赖,但能让问题更早暴露:
@Service
public class UserService {
private final UserMapper userMapper;
private final OrderService orderService;
@Autowired
public UserService(UserMapper userMapper, OrderService orderService) {
this.userMapper = userMapper;
this.orderService = orderService;
}
}
真实案例:我之前在一个电商项目中就遇到过这个问题。用户服务和订单服务之间因为查询逻辑互相引用,项目启动直接报错。最后我们把共用的查询逻辑抽离到一个独立的OrderQueryService中,问题迎刃而解。记住,循环依赖是设计问题的信号,不要只是用@Lazy去掩盖它。
坑二:事务管理不知道怎么加
学完Spring之后,你知道有事务这个概念,知道ACID特性,知道@Transactional注解。但是真正用的时候,完全不知道该怎么用。
比如你写了一个用户注册方法,需要同时插入用户信息和用户详情信息:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserProfileMapper userProfileMapper;
public void register(User user, UserProfile profile) {
userMapper.insert(user);
profile.setUserId(user.getId());
userProfileMapper.insert(profile);
}
}
如果第二步失败了,第一步的数据就变成孤儿数据了。你记得要加事务,于是在方法上加上@Transactional:
@Transactional
public void register(User user, UserProfile profile) {
userMapper.insert(user);
profile.setUserId(user.getId());
userProfileMapper.insert(profile);
}
看起来对吗?不对。你很可能发现事务根本没生效。
常见原因一:注解加在了非public方法上
@Transactional
protected void register(User user, UserProfile profile) { // 报错:事务不生效
...
}
Spring的AOP代理只对public方法生效,protected、private方法上的@Transactional会被忽略。
常见原因二:同类方法调用
@Service
public class UserService {
public void createUser(User user) {
this.register(user); // 事务不生效!
}
@Transactional
public void register(User user) {
// 数据库操作
}
}
当你在同一个类内部调用带有@Transactional的方法时,Spring的代理机制绕不过去,事务不会生效。解决方案是用@Lazy注入自身,或者把事务方法抽到另一个类中。
常见原因三:异常被捕获但没有抛出
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
try {
userMapper.deductMoney(fromId, amount);
userMapper.addMoney(toId, amount);
} catch (Exception e) {
// 异常被吞掉了,事务不会回滚!
log.error("转账失败", e);
}
}
@Transactional默认只回滚RuntimeException和Error,如果你捕获了异常却没有重新抛出,事务不会回滚。
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
try {
userMapper.deductMoney(fromId, amount);
userMapper.addMoney(toId, amount);
} catch (Exception e) {
log.error("转账失败", e);
throw new RuntimeException("转账失败", e); // 必须重新抛出
}
}
或者你可以指定回滚的异常类型:
@Transactional(rollbackFor = Exception.class)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
userMapper.deductMoney(fromId, amount);
userMapper.addMoney(toId, amount);
}
坑三:MyBatis映射总是找不到
当你把业务逻辑写好了,开始对接数据库,使用MyBatis进行数据操作。结果启动报错,或者运行时报错:
Invalid bound statement (not found): com.example.mapper.UserMapper.selectById
这个错误太经典了,新手几乎都会遇到。
原因一:Mapper接口没有被扫描到
你需要在启动类或者配置类上添加Mapper扫描:
@SpringBootApplication
@MapperScan("com.example.mapper") // 必须添加这个
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
或者在每个Mapper接口上加@Mapper注解:
@Mapper
public interface UserMapper {
User selectById(Long id);
}
原因二:XML文件路径问题
如果你用的是XML方式的Mapper,需要确保XML文件的位置正确,并且在配置中指定了扫描路径:
# application.yml
mybatis:
mapper-locations: classpath:mapper/*.xml # 扫描所有XML文件
type-aliases-package: com.example.entity # 实体类包扫描
原因三:方法名和XML中的statement ID不一致
这是最容易忽略的。XML中的id必须和接口中的方法名完全一致:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
// UserMapper.java
public interface UserMapper {
User selectById(Long id); // 必须和XML中的id一致
}
原因四:XML文件没有被打包到classpath中
如果你用的是Maven项目,XML文件放在了src/main/java目录下,可能会被忽略。确保XML文件在src/main/resources目录下,或者在pom.xml中配置资源过滤:
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
坑四:实体类设计一塌糊涂
很多人刚开始写项目的时候,实体类就是简单地把数据库字段映射成Java属性:
public class User {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private Date createTime;
private Date updateTime;
}
这本身没错,但实际项目中问题就来了。
问题一:密码明文存储
千万不要直接把密码存进数据库!你在Controller或Service中接收的密码参数,应该在进行业务处理之前先加密:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder; // Spring Security提供的密码编码器
public void register(User user) {
// 加密密码
String rawPassword = user.getPassword();
user.setPassword(passwordEncoder.encode(rawPassword));
userMapper.insert(user);
}
}
然后在Spring配置中声明PasswordEncoder:
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
问题二:时间字段处理
数据库中的时间字段,Java中的Date类型用起来很麻烦。推荐使用LocalDateTime:
public class User {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
在MyBatis中配合使用@DateTimeFormat和@JsonFormat注解:
public class User {
// 数据库查询结果映射到Java对象时格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
// 接收前端传入的时间参数时解析
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
在MyBatis配置中启用Java 8时间类型支持:
mybatis:
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰
type-handlers-package: org.apache.ibatis.type # 类型处理器
或者使用MyBatis-Plus,它已经内置了对Java 8时间类型的支持。
问题三:实体类职责不清晰
实体类(Entity/PO)应该只做数据库字段的映射,不要在里面写业务逻辑。很多新手喜欢在实体类里加各种方法:
public class User {
private String password;
// 错误示范:实体类不应该有业务逻辑
public boolean checkPassword(String input) {
return BCrypt.checkpw(input, this.password);
}
public void setPassword(String password) {
this.password = BCrypt.hashpw(password, BCrypt.gensalt());
}
}
正确做法是把业务逻辑放在Service层,实体类保持纯粹:
public class User {
private Long id;
private String username;
private String password;
// getter和setter...
}
@Service
public class UserService {
public boolean checkPassword(User user, String inputPassword) {
return BCrypt.checkpw(inputPassword, user.getPassword());
}
}
坑五:Controller层写得像面条
学完Spring MVC之后,你知道Controller负责接收请求、调用Service、返回结果。但很多新手写的Controller特别乱:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper; // Controller里直接操作Mapper,错误!
@Autowired
private OrderMapper orderMapper; // 还依赖了OrderMapper?
@Autowired
private UserService userService;
@GetMapping("/list")
public List<User> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
// 参数校验?没有
// 分页查询?直接在Mapper里写了个奇怪的方法
// 结果处理?直接返回List
return userService.listUsers(page, size);
}
@PostMapping("/register")
public Result register(@RequestBody User user) {
// 校验逻辑写在Controller里?
if (StringUtils.isBlank(user.getUsername())) {
return Result.error("用户名不能为空");
}
if (StringUtils.isBlank(user.getPassword())) {
return Result.error("密码不能为空");
}
if (!user.getPassword().equals(user.getConfirmPassword())) {
return Result.error("两次密码不一致");
}
// 这么多校验逻辑,应该抽出去
userService.register(user);
return Result.success();
}
}
问题一:Controller依赖了不该依赖的东西
Controller只应该依赖Service,不应该直接操作Mapper。Mapper是数据访问层的实现细节,不应该暴露给Controller:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService; // 只依赖Service
@GetMapping("/list")
public PageResult<User> list(@Validated UserQueryDTO queryDTO) {
return userService.queryUsers(queryDTO);
}
}
问题二:参数校验逻辑混乱
参数校验不应该写在Controller里,应该使用Bean Validation框架(JSR-303/380):
public class UserRegisterDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
@Email(message = "邮箱格式不正确")
private String email;
// getter和setter...
}
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@Validated @RequestBody UserRegisterDTO dto) {
userService.register(dto);
return Result.success();
}
}
然后全局异常处理器统一处理校验失败的情况:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(400, message);
}
}
坑六:分层结构混乱,代码到处乱飞
学完Spring之后,你知道有Controller、Service、Mapper三层架构。但真正写项目的时候,很容易越界:
controller层:调用service,调用mapper,还自己写SQL
service层:调用mapper,还直接操作HttpServletRequest
mapper层:写业务逻辑,不只是数据访问
正确的分层应该是这样的:
controller层:接收请求参数,参数校验,调用service,返回响应
service层:业务逻辑处理,事务管理,调用mapper
mapper层:数据访问,只做CRUD操作,不含业务逻辑
具体示例:
// Controller层 - 只做请求处理
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public PageResult<UserVO> list(@Validated UserQueryDTO dto) {
return userService.queryUsers(dto);
}
@PostMapping
public Result create(@Validated @RequestBody UserCreateDTO dto) {
userService.createUser(dto);
return Result.success();
}
}
// Service层 - 业务逻辑
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
public PageResult<UserVO> queryUsers(UserQueryDTO dto) {
// 业务逻辑:分页查询、条件过滤等
Page<User> page = userMapper.selectPage(dto.toPage());
return page.to(UserVO.class);
}
@Transactional
public void createUser(UserCreateDTO dto) {
// 业务逻辑:密码加密、数据组装、事务控制
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setEmail(dto.getEmail());
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
// 额外业务逻辑:发送欢迎邮件、创建默认配置等
sendWelcomeEmail(user.getEmail());
}
private void sendWelcomeEmail(String email) {
// 发邮件逻辑
}
}
// Mapper层 - 只做数据访问
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 只定义数据访问方法,不含业务逻辑
List<User> selectByCondition(@Param("condition") UserQueryDTO condition);
}
坑七:异常处理一团糟
很多新手写异常处理的方式是直接try-catch到处飘:
public User getUserById(Long id) {
try {
return userMapper.selectById(id);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
或者在Controller层大量try-catch:
@GetMapping("/{id}")
public Result getById(@PathVariable Long id) {
try {
User user = userService.getById(id);
if (user == null) {
return Result.error("用户不存在");
}
return Result.success(user);
} catch (Exception e) {
return Result.error("查询失败");
}
}
这种写法的问题很明显:异常信息丢失、错误码不统一、维护困难。
正确做法:使用全局异常处理器:
// 自定义业务异常
public class BusinessException extends RuntimeException {
private final Integer code;
private final String message;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
// 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldError().getDefaultMessage();
log.warn("参数校验失败: {}", message);
return Result.error(400, message);
}
// 数据库异常
@ExceptionHandler(DataIntegrityViolationException.class)
public Result handleDataIntegrityException(DataIntegrityViolationException e) {
log.error("数据完整性异常", e);
return Result.error(500, "数据操作失败,请检查数据是否正确");
}
// 通用异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "系统异常,请联系管理员");
}
}
然后业务代码中只需要抛出异常,不需要自己处理:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
public User getById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(404, "用户不存在");
}
return user;
}
@Transactional
public void createUser(UserCreateDTO dto) {
// 参数校验
if (StringUtils.isBlank(dto.getUsername())) {
throw new BusinessException(400, "用户名不能为空");
}
if (userMapper.selectByUsername(dto.getUsername()) != null) {
throw new BusinessException(400, "用户名已存在");
}
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
}
}
坑八:配置管理混乱
新手写项目,配置信息到处写死:
public class UserService {
private final String jwtSecret = "my-secret-key-123456";
private final int tokenExpiration = 7200;
private final String uploadPath = "/data/uploads";
}
或者在application.properties里写得很乱:
# 乱七八糟的配置
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=123456
jwt.secret=my-secret-key
jwt.expiration=7200
upload.path=/data/uploads
oss.bucket=my-bucket
oss.endpoint=oss-cn-hangzhou.aliyuncs.com
oss.access-key=LTAI5xxx
oss.access-secret=xxxx
正确的做法:
第一,配置文件要分类管理,使用Spring Boot的配置绑定机制:
// 配置属性类
@Data
@ConfigurationProperties(prefix = "app.jwt")
public class JwtProperties {
private String secret;
private long expiration;
}
@Data
@ConfigurationProperties(prefix = "app.upload")
public class UploadProperties {
private String path;
private String bucket;
private String endpoint;
}
# application.yml - 按模块分组
app:
jwt:
secret: ${JWT_SECRET:default-secret-key-change-in-production}
expiration: 7200
upload:
path: ${UPLOAD_PATH:/data/uploads}
bucket: ${OSS_BUCKET:my-bucket}
endpoint: ${OSS_ENDPOINT:oss-cn-hangzhou.aliyuncs.com}
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:mydb}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:123456}
第二,敏感信息(密码、密钥)不要硬编码,使用环境变量或配置中心:
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user) {
return Jwts.builder()
.setSubject(String.valueOf(user.getId()))
.setIssuer("my-app")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 1000))
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret().getBytes())
.compact();
}
}
第三,不同环境使用不同的配置文件:
# application-dev.yml - 开发环境
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: root
password: dev_password
# application-prod.yml - 生产环境
spring:
datasource:
url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
坑九:日志不会打,排查问题全靠println
新手写代码,调试靠System.out.println,线上出了问题不知道哪里错了。
public void createUser(UserCreateDTO dto) {
System.out.println("开始创建用户:" + dto.getUsername()); // 这是什么行为?
User user = new User();
user.setUsername(dto.getUsername());
userMapper.insert(user);
System.out.println("用户创建成功,ID:" + user.getId());
}
正确做法:使用SLF4J + Logback:
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserMapper userMapper;
public User getById(Long id) {
log.info("查询用户,ID: {}", id); // 信息级别日志
User user = userMapper.selectById(id);
if (user == null) {
log.warn("用户不存在,ID: {}", id); // 警告级别
throw new BusinessException(404, "用户不存在");
}
return user;
}
@Transactional
public void createUser(UserCreateDTO dto) {
log.info("开始创建用户,username: {}", dto.getUsername());
try {
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
log.info("用户创建成功,ID: {}", user.getId());
} catch (Exception e) {
log.error("用户创建失败,username: {}", dto.getUsername(), e); // 错误级别,带堆栈
throw new BusinessException(500, "用户创建失败: " + e.getMessage());
}
}
}
日志级别的使用原则:
debug:调试信息,开发环境开启,生产环境关闭info:关键业务流程,生产环境必须开启warn:潜在问题,需要关注但不影响运行error:错误信息,必须记录,通常带异常堆栈
application.yml中的日志配置:
logging:
level:
root: INFO
com.example: DEBUG
org.springframework: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/app.log
坑十:项目结构从零开始,不知道该怎么组织
很多人学完Spring之后,第一个项目不知道怎么组织包结构。这里给你一个通用的模板:
com.example.project
├── common # 公共模块
│ ├── config # 配置类
│ │ ├── MybatisPlusConfig.java
│ │ ├── SecurityConfig.java
│ │ ├── JwtConfig.java
│ │ └── SwaggerConfig.java
│ ├── exception # 异常处理
│ │ ├── BusinessException.java
│ │ └── GlobalExceptionHandler.java
│ ├── result # 统一返回结果
│ │ ├── Result.java
│ │ └── PageResult.java
│ └── utils # 工具类
│ ├── DateUtils.java
│ └── StringUtils.java
├── module # 业务模块
│ ├── user # 用户模块
│ │ ├── controller # 控制层
│ │ │ └── UserController.java
│ │ ├── service # 业务层
│ │ │ ├── UserService.java
│ │ │ └── impl # 实现类
│ │ │ └── UserServiceImpl.java
│ │ ├── mapper # 数据访问层
│ │ │ └── UserMapper.java
│ │ ├── entity # 实体类
│ │ │ └── User.java
│ │ ├── dto # 数据传输对象
│ │ │ ├── UserCreateDTO.java
│ │ │ ├── UserQueryDTO.java
│ │ │ └── UserVO.java
│ │ └── vo # 视图对象(可选,和DTO合并也可)
│ │ └── UserVO.java
│ └── order # 订单模块(结构同上)
├── application.yml # 应用配置
└── Application.java # 启动类
每一层都有明确的职责:
- Controller:接收请求,参数校验,调用Service,返回结果
- Service:业务逻辑,事务控制,调用Mapper
- Mapper:数据访问,CRUD操作
- Entity:数据库表映射
- DTO:数据传输对象(Controller和Service之间)
- VO:视图对象(Service和Controller之间)
DTO和VO的区别:DTO用于接收前端传入的参数,VO用于封装返回给前端的响应数据。它们和Entity的区别在于,DTO/VO只包含需要的字段,不进行全量映射,避免数据泄露:
// Entity:和数据库表一一对应,包含所有字段
public class User {
private Long id;
private String username;
private String password; // 敏感字段
private String email;
private String phone;
private Integer status;
private LocalDateTime createTime;
}
// DTO:只包含前端需要传入的字段
public class UserCreateDTO {
@NotBlank
private String username;
@NotBlank
private String password;
@Email
private String email;
}
// VO:只包含前端需要展示的字段,不包含敏感信息
public class UserVO {
private Long id;
private String username;
private String email;
private Integer status;
private LocalDateTime createTime;
// 没有password字段
}
坑十一:不会用Postman/接口调试工具
写好了接口,不知道怎么测试。很多人直接写个main方法测试,或者用浏览器GET请求测试所有接口。
推荐使用Postman或Apifox:
创建一个Collection,按模块分组:
- 用户模块
- 用户注册 POST /api/users/register
- 用户登录 POST /api/users/login
- 查询用户列表 GET /api/users
- 查询用户详情 GET /api/users/{id}
- 更新用户 PUT /api/users/{id}
- 删除用户 DELETE /api/users/{id}
- 订单模块
- 创建订单 POST /api/orders
- 查询订单 GET /api/orders
...
每个接口的请求体用JSON格式:
{
"username": "zhangsan",
"password": "123456",
"email": "zhangsan@example.com"
}
响应格式统一:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"username": "zhangsan",
"email": "zhangsan@example.com"
}
}
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
坑十二:部署和上线懵圈
开发环境跑通了,不知道怎么部署到生产环境。
基本的部署流程:
# 1. 打包
mvn clean package -DskipTests
# 2. 生成JAR包
# target/your-app-1.0.0.jar
# 3. 上传到服务器
scp target/your-app-1.0.0.jar user@server:/opt/app/
# 4. 启动
nohup java -jar your-app-1.0.0.jar --spring.profiles.active=prod > app.log 2>&1 &
# 5. 查看日志
tail -f app.log
或者使用Docker部署:
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
# 构建镜像
docker build -t my-app:1.0.0 .
# 运行容器
docker run -d \
-p 8080:8080 \
-e DB_HOST=192.168.1.100 \
-e DB_PASSWORD=prod_password \
--name my-app \
my-app:1.0.0
给你的实战建议
别盯着教程看了,动手做一个完整的项目是最好的学习方式。我给你一个推荐的项目路线:
第一阶段:用户管理系统
- 用户注册、登录、权限控制
- 学习Spring Security + JWT
- 学习密码加密、Token生成和验证
第二阶段:博客系统
- 文章CRUD、分类标签
- 学习分页查询、条件搜索
- 学习文件上传(头像、文章图片)
第三阶段:电商系统
- 商品管理、购物车、订单
- 学习事务管理、库存扣减
- 学习Redis缓存、消息队列
第四阶段:微服务架构
- 拆分服务、RPC调用
- 学习Spring Cloud
- 学习服务注册发现、配置中心
每个阶段都有明确的目标和需要掌握的技术点,不要一上来就想做大型项目,循序渐进才能打好基础。
总结一下
从HelloWorld到实际项目,你需要跨越的不仅仅是技术鸿沟,更是工程化思维的建立。记住以下几点:
- 分层清晰:Controller、Service、Mapper各司其职,不要越界
- 异常统一:全局异常处理器,不要到处try-catch
- 配置分离:敏感信息放环境变量,不同环境用不同配置
- 日志规范:该记什么级别、记什么内容,心里有数
- 工具熟练:Postman、Git、Maven、Docker,这些都要会用
- 多看源码:Spring的源码、优秀开源项目的代码结构,能帮你理解设计思路
最后说一句,你现在的困惑,几乎每个程序员都经历过。不要怕犯错,不要怕踩坑,每一个坑都是成长的机会。遇到报错先自己排查,查文档、看源码、搜StackOverflow,实在解决不了再问人。这个过程积累的经验,比看十遍教程都有用。
加油吧,未来的大牛!🚀
