学完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默认只回滚RuntimeExceptionError,如果你捕获了异常却没有重新抛出,事务不会回滚。

@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到实际项目,你需要跨越的不仅仅是技术鸿沟,更是工程化思维的建立。记住以下几点:

  1. 分层清晰:Controller、Service、Mapper各司其职,不要越界
  2. 异常统一:全局异常处理器,不要到处try-catch
  3. 配置分离:敏感信息放环境变量,不同环境用不同配置
  4. 日志规范:该记什么级别、记什么内容,心里有数
  5. 工具熟练:Postman、Git、Maven、Docker,这些都要会用
  6. 多看源码:Spring的源码、优秀开源项目的代码结构,能帮你理解设计思路

最后说一句,你现在的困惑,几乎每个程序员都经历过。不要怕犯错,不要怕踩坑,每一个坑都是成长的机会。遇到报错先自己排查,查文档、看源码、搜StackOverflow,实在解决不了再问人。这个过程积累的经验,比看十遍教程都有用。

加油吧,未来的大牛!🚀