Skip to content
第 45 章 后端 ⏱ 8 分钟阅读

第 45 章:项目结构与配置 ​

学习目标 ​

  • 学会企业级项目目录结构
  • 掌握 yml 配置语法
  • 学会用 @Value / @ConfigurationProperties

一、标准项目结构 ​

src/main/java/com/example/myapp/
├── MyAppApplication.java         启动类
├── config/                       配置类
│   ├── WebConfig.java
│   └── RedisConfig.java
├── controller/                   控制器
│   └── UserController.java
├── service/                      业务层
│   ├── UserService.java
│   └── impl/
│       └── UserServiceImpl.java
├── mapper/                       数据访问层
│   └── UserMapper.java
├── entity/                       实体类
│   └── User.java
├── dto/                          入参
│   └── UserDTO.java
├── vo/                           出参
│   └── UserVO.java
├── common/                       公共类
│   ├── Result.java
│   ├── ErrorCode.java
│   └── BusinessException.java
└── utils/                        工具类
    └── JwtUtils.java

src/main/resources/
├── application.yml               主配置
├── application-dev.yml           开发
├── application-prod.yml          生产
├── static/                       静态资源
├── templates/                    模板(很少用)
└── mapper/                       MyBatis XML

⚠️ 坑 1:controller 调 service,service 调 mapper。绝对不允许 controller 直接调 mapper。

二、Controller / Service / Mapper 分层 ​

java
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;   // 注入 service

    @GetMapping("/{id}")
    public Result<UserVO> getById(@PathVariable Long id) {
        User user = userService.getById(id);
        return Result.ok(UserVO.from(user));
    }
}

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserMapper userMapper;    // 注入 mapper

    public User getById(Long id) {
        return userMapper.selectById(id);
    }
}

public interface UserMapper extends BaseMapper<User> { }   // MyBatis Plus

为什么分层?

  • Controller:接 HTTP,参数校验,转换 VO
  • Service:业务逻辑,事务
  • Mapper:纯 SQL,不写业务

三、配置读取 ​

3.1 @Value(单个值) ​

yaml
app:
  jwt:
    secret: my-secret-key
    expire: 7200
java
@Value("${app.jwt.secret}")
private String jwtSecret;

@Value("${app.jwt.expire:7200}")  // 默认值
private int jwtExpire;

3.2 @ConfigurationProperties(批量绑定,推荐) ​

java
@Data
@Component
@ConfigurationProperties(prefix = "app.jwt")
public class JwtProperties {
    private String secret;
    private int expire;
}
java
@RequiredArgsConstructor
@Service
public class JwtService {
    private final JwtProperties jwtProperties;   // 自动注入配置

    public String createToken(String userId) {
        return jwtProperties.getSecret() + ":" + userId;  // 简化示意
    }
}

为什么推荐 @ConfigurationProperties?

  • 编译期类型检查(写错字段名会报错)
  • IDE 自动补全
  • 集中管理一类配置

3.3 自定义 starter 配置(给团队用) ​

这是"给别的项目用"的写法,别和 3.2 混了。 3.2 的 @Component / @Service 靠组件扫描注册,只在自己项目里有效;一旦打包成 jar 给别人用,别人项目的组件扫描扫不到你 jar 里的类,所以必须改成"装配类主动注册"。类比 npm 包:你写库、打包、发布,别人加依赖 + 填配置就能直接用。

场景:你写了个 JWT 工具库,打成 jar 放到 Maven 仓库,团队其他项目加个依赖就能用,不用配任何东西。

别人项目里发生了什么——他们只做三件事,一行配置代码都不写:

text
1. pom.xml 加依赖: jwt-spring-boot-starter
2. application.yml 填配置:
     app:
       jwt:
         secret: my-secret-key
         expire: 7200
3. 直接注入就能用: @Autowired JwtService jwtService

但为什么他们什么都不用配就能用? 因为启动时 Spring Boot 会去读你 jar 里的一个清单文件:

text
jar 里 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    内容: com.mylib.jwt.JwtAutoConfiguration   ← 告诉 Spring "启动时装配这个类"

jar 里(库作者写)的三件套:

java
// ① 配置类:把 app.jwt.* 绑成对象。注意:这里不写 @Component!
//    因为别人项目扫描不到,改用下面的 @EnableConfigurationProperties 主动注册
@Data
@ConfigurationProperties(prefix = "app.jwt")
public class JwtProperties {
    private String secret;
    private int expire;
}

// ② 服务类:普通类,【不能写 @Service】!@Service 靠组件扫描,打包进 jar 就失效。
//    它由下面的 @Bean 创建
public class JwtService {
    private final JwtProperties properties;
    public JwtService(JwtProperties properties) { this.properties = properties; }
    public String createToken(String userId) { return properties.getSecret() + ":" + userId; }
}

// ③ 装配类(核心):把 ①② 主动注册并交给 Spring
@Data
@Configuration                                        // 装配车间,由清单文件触发
@EnableConfigurationProperties(JwtProperties.class)   // 主动注册 JwtProperties + 绑定 app.jwt.*
public class JwtAutoConfiguration {

    @Bean                                           // 创建 JwtService 交给 Spring
    public JwtService jwtService(JwtProperties properties) {
        return new JwtService(properties);
    }
}

为什么是这三件套 / 各管什么:

  • JwtProperties:数据载体,@ConfigurationProperties 绑定 app.jwt.*。给别人用时不写 @Component,改由 @EnableConfigurationProperties 注册(因为它不依赖包扫描)。
  • JwtService:业务逻辑,不写 @Service(jar 里扫描失效),由 @Bean 方法 new 出来。
  • JwtAutoConfiguration:装配车间,@EnableConfigurationProperties 注册配置类 + @Bean 注册服务类,靠 AutoConfiguration.imports 清单自动触发。

⚠️ 坑:别在 jar 里的 JwtService 上加 @Service、在 JwtProperties 上加 @Component——这些注解靠组件扫描,打包给别人就全部失效。规则:库里的类一律用"装配类 + @Bean + @EnableConfigurationProperties"注册。

四、配置加密(Jasypt) ​

xml
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>
yaml
jasypt:
  encryptor:
    password: ${JASYPT_PASSWORD}    # 启动时从环境变量读

spring:
  datasource:
    password: ENC(加密后的密文)     # 加密后用 ENC() 包起来

加密(命令行):

bash
java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \
  input="my-password" password="my-key" algorithm=PBEWithMD5AndDES

⚠️ 坑 2:密钥不能写死在 yml 里。从环境变量 / K8s Secret / 配置中心读。

五、跨配置文件共享 ​

yaml
# application.yml
spring:
  profiles:
    active: dev
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:myapp}

${DB_HOST:localhost} = 优先用环境变量 DB_HOST,没有就用 localhost。

六、读取配置文件中的 List / Map ​

yaml
app:
  cors:
    origins:
      - https://app.example.com
      - https://admin.example.com
java
@Data
@ConfigurationProperties(prefix = "app.cors")
public class CorsProperties {
    private List<String> origins;
}

七、本章小结 ​

要点关键
分层Controller → Service → Mapper
目录controller/service/mapper/entity/dto/vo/common
配置@ConfigurationProperties 批量绑定
加密Jasypt + 环境变量
环境application-{profile}.yml

动手练习 ​

  1. 创建一个项目,按标准分层创建目录,每个目录写一个示例文件
  2. 把所有 @Value 改成 @ConfigurationProperties
  3. 用 Jasypt 加密数据库密码

下一章:第 46 章:日志与链路追踪 →

本站基于 VitePress 构建 · 由 StackHub 团队维护