第 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: 7200java
@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.comjava
@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 |
动手练习
- 创建一个项目,按标准分层创建目录,每个目录写一个示例文件
- 把所有
@Value改成@ConfigurationProperties - 用 Jasypt 加密数据库密码
下一章:第 46 章:日志与链路追踪 →