Spring Boot
Spring Boot 把「容器管对象(IoC/DI)→ MVC 接请求 → ORM/映射器落数据 → Security 做鉴权」串成一条生产链路。本文用一个最小可运行工程(OpenJDK 17.0.20 / Spring Boot 3.3.5 / Maven 3.9.16,依赖 spring-boot-starter-web + data-jpa + security + MyBatis + H2 内存库)逐层实测:构造器注入、REST 路由、JPA/MyBatis 同表双写、Security 过滤链的 401/200 行为。
一、IoC 容器与依赖注入
核心概念
Spring 的 IoC(控制反转):对象不由调用方 new,而是交给容器创建并管理生命周期;需要时按类型注入(DI)。容器里的对象叫 Bean,默认单例(容器启动时创建一份,之后共享)。
注册与取用的三件套注解:
| 注解 | 作用 | 典型位置 |
|---|---|---|
@Service / @Component | 声明这是一个可注入 Bean | 业务服务、通用组件 |
@Repository | 数据访问层 Bean(异常转 Spring DAO 体系) | DAO / Mapper |
@RestController / @Controller | Web 层,同时注册为 Bean | 接口入口 |
依赖注入三种姿势:
// 1. 构造器注入(推荐:依赖不可变、便于单测、容器保证构造时齐备)
@Service
public class GreetingService {
public String greet(String name) {
return "Hello, " + (name == null || name.isBlank() ? "World" : name) + "!";
}
}
@RestController
public class GreetingController {
private final GreetingService service; // final:构造后不再变
public GreetingController(GreetingService service) { this.service = service; } // ← 容器自动传入
// ...
}// 2. Setter 注入:可选依赖、需要运行时替换时用
// 3. 字段注入(@Autowired 直接打在字段上):最省事但难测、依赖隐式——新代码避免Spring 官方与多数团队规范都要求构造器注入:能发现"漏依赖"(构造参数缺失直接启动失败而非运行时 NPE)、字段可
final、测试时手动new即可注入 mock。
实测:注入链路与启动装配
容器在启动阶段一次性完成「Bean 实例化 → 依赖解析 → 注入 → 路由映射」,最小工程实测启动日志:
INFO o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 18080 (http) with context path '/'
INFO com.example.demo.DemoApplication : Started DemoApplication in 5.755 seconds没有报 No qualifying bean of type 'GreetingService' 即说明注入成功——若把 @Service 去掉,启动会直接失败,这正是构造器注入让依赖缺失在启动期暴露的效果。
坑
- 循环依赖:A→B→A。构造器注入的循环依赖无法自动解决(容器直接报错),需重新设计(下沉公共 Bean 或用
@Lazy打破)——尽早让启动失败暴露它; - Bean 默认单例:不要在单例里存请求级可变状态(多线程共享),要用
@Scope("prototype")/@Scope("request")显式声明; @Configuration里的@Bean方法之间互相调用要依赖容器代理,别在一个@Bean方法里直接new另一个配置里的 Bean;- 忘了加组件扫描注解或把类放到扫描包外,Bean 不会被注册,注入点为 null。
二、Spring MVC 与 REST API 设计
分层与注解
经典三层:Controller(收参数/转状态码)→ Service(业务事务)→ Repository(数据)。
| 注解 | 作用 | 示例 |
|---|---|---|
@RestController | JSON API:返回值直接序列化为响应体 | 类上 |
@GetMapping / @PostMapping 等 | 方法与 HTTP 方法+路径绑定 | @GetMapping("/members/{id}") |
@PathVariable | 取路径段 | {id} → Long id |
@RequestParam | 取查询串 | ?name=x → String name |
@RequestBody | 反序列化请求体为对象 | @RequestBody Member body |
@Valid + DTO | 参数校验(配合 starter-validation) | @Valid @RequestBody |
@RestController
@RequestMapping("/members") // 类级前缀
public class MemberController {
private final MemberRepository repository;
public MemberController(MemberRepository repository) { this.repository = repository; }
@GetMapping
public List<Member> list() { return repository.findAll(); }
@PostMapping
public Member create(@RequestBody Member body) {
return repository.save(new Member(body.getName()));
}
}统一异常处理(返回结构化错误而非默认错误页):
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, Object> notFound(NotFoundException e) {
return Map.of("error", e.getMessage(), "status", 404);
}
}实测:路由行为
$ curl "http://127.0.0.1:18080/greet?name=Spring" # GET 公开端点
Hello, Spring! (from 1788350200660)
$ curl http://127.0.0.1:18080/greet # 缺省参数兜底
Hello, World!坑
@RestController=@Controller+@ResponseBody:用@Controller而忘加@ResponseBody会把返回值当视图名去找模板,返回 500;- 直接把 JPA 实体当响应体返回:懒加载字段序列化时抛
LazyInitializationException、还会把不该暴露的内部字段漏出去——用 DTO/Record 做进出口; - 别用
@RequestParam收 JSON body、也别用@RequestBody收表单,二者不能混; - REST 语义:查询用 GET、创建用 POST、幂等更新用 PUT、局部更新用 PATCH、删除用 DELETE,并配合合适的状态码(200/201/204/4xx/5xx)。
三、数据访问:JPA 还是 MyBatis
JPA(对象关系映射)
声明一个接口继承 JpaRepository,Spring Data 在启动时动态生成实现:save/findById/findAll/deleteById 全都有;按方法名派生查询(findByName、findByAgeGreaterThan)自动翻译成 SQL。
@Entity
@Table(name = "member")
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getter/setter…
}
public interface MemberRepository extends JpaRepository<Member, Long> {
// 派生查询:SELECT * FROM member WHERE name = ?
List<Member> findByName(String name);
}MyBatis(SQL 映射)
SQL 由你显式书写,注解或 XML 两种风格。适合复杂多表 SQL、需要对 SQL 精细控制的老系统与 DBA 协作场景。
@Mapper
public interface MemberMapper {
@Select("SELECT COUNT(*) FROM member")
long count();
@Insert("INSERT INTO member(name) VALUES(#{name})")
int insert(@Param("name") String name);
}实测:JPA 与 MyBatis 同表双写
H2 内存库,ddl-auto=create-drop 由 JPA 自动建表,两个访问层写同一张 member 表:
[data] JPA member 数量 = 3 ← JPA save 了 2 条 + MyBatis insert 了 1 条
[data] MyBatis member 数量 = 3 ← 两条链路读同一张表,结果一致
[data] JPA 注入示例: repository 实例 = MemberRepositoryGET /members(认证后)返回:
[{"id":1,"name":"jpa-member"},{"id":2,"name":"jpa-member-2"},{"id":3,"name":"mybatis-member"}]选型对比
| 维度 | Spring Data JPA | MyBatis |
|---|---|---|
| 思维模型 | 面向对象,框架生成 SQL | 面向 SQL,你写语句 |
| 简单 CRUD | 零代码(接口继承) | 每方法写 SQL/映射 |
| 复杂查询/动态 SQL | 需要 Specifications/QueryDSL,学习成本高 | if/where/foreach 天然支持 |
| 性能可控性 | 弱(容易 N+1、隐式全表字段) | 强(SQL 与表结构完全可见) |
| 学习曲线 | 入门快、深入难(缓存/懒加载陷阱多) | 上手要写 SQL,思路直接 |
个人项目/新系统起步常选 JPA(少样板),业务复杂 SQL 占比高或老库多表关联用 MyBatis(如国内大量企业)。两者能共存,但一个工程里别混着用同一张表(本项目为演示才同表双写)。
坑
- N+1 查询:遍历父实体读子集合时,懒加载逐条触发查询(1+ N 条 SQL)——用
@EntityGraph/join fetch/ 批量抓取解决(SQL 侧同样问题见数据库页的 N+1 实测); @Transactional只在通过代理调用时生效:同类内this自调用、private方法、非 Spring 管理的对象上都失效;异常默认只回滚RuntimeException,checked 异常需rollbackFor;- JPA 实体直接做前端入参/出参:既破坏封装又可能触发懒加载异常,出入都用 DTO;
- MyBatis 的
#{}(预编译占位)与${}(字符串拼接,有注入风险)要分清:用户输入一律#{}。
四、Spring Security:过滤链与鉴权
核心概念
Spring Security 由一串 Filter 组成过滤链(SecurityFilterChain):认证(你是谁)→ 授权(你能干什么)→ CSRF/异常处理等横切。对无状态 REST API,常规配置是:关 CSRF、声明放行白名单、其余全部走认证(HTTP Basic 或 JWT 都在这条链上拦截)。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable()) // 无状态 API:没有 Cookie 会话,CSRF 无意义
.authorizeHttpRequests(auth -> auth
.requestMatchers("/greet", "/public/**").permitAll() // 白名单
.anyRequest().authenticated()) // 其余都要登录
.httpBasic(Customizer.withDefaults()); // 演示用 Basic;生产常换 JWT 过滤器
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 密码绝不明文存
}
@Bean
public UserDetailsService users(PasswordEncoder encoder) {
return new InMemoryUserDetailsManager(
User.withUsername("admin").password(encoder.encode("secret")).roles("USER").build());
}
}实测:过滤链行为
启动时打印 BCrypt 哈希(每次盐不同,哈希不同,数据库比对靠 matches 而不是等值):
[security] BCrypt hash of 'secret' = $2a$10$SWy5v/QR1kWZWUGKBwaFxeSYUi/0hb24.mNno6y9yXmCWKqb65ZE6| 请求 | 凭据 | 结果 |
|---|---|---|
GET /greet(白名单) | 无 | 200 Hello, Spring! |
GET /members | 无 | 401 Unauthorized |
GET /members | admin:secret | 200 JSON 成员列表 |
GET /members | admin:wrong | 401 Unauthorized |
POST /members(带 Basic) | admin:secret | 201/200 返回新建成员 {"id":4,...} |
生产 JWT 路线的思路:写一个
OncePerRequestFilter解析Authorization: Bearer xxx→ 校验签名 → 把用户信息放进SecurityContextHolder;过滤链对"已认证上下文"直接放行,与上面的 401 拦截逻辑完全一致。更完整的 JWT/OAuth2 对比见鉴权认证。
坑
- 引入
spring-boot-starter-security后默认全部端点都需认证——没配白名单前先别急着调接口,先看 401; - 登录密码要哈希:BCrypt 自带盐且慢哈希抗暴力;不要自研"加密"(可逆)当"哈希"存密码;
- REST 无状态接口要关 CSRF(依赖 Cookie 会话的浏览器应用才需要 CSRF 防护),否则 POST/PUT 会莫名 403;
- 前端跨域调接口要配 CORS(
cors()结合@CrossOrigin或CorsConfigurationSource),否则浏览器拦截响应; - 不要用
{noop}前缀存明文密码(Spring 允许但会警告),上线前全量迁移为 BCrypt。
五、最小工程结构参考(本文实测工程)
sb-demo/
├── pom.xml # parent=spring-boot-starter-parent 3.3.5
│ └── 依赖:web / validation / data-jpa / security / mybatis-spring-boot-starter 3.0.4 / h2(runtime)
├── src/main/resources/application.properties
│ └── server.port=18080;H2 内存库;spring.jpa.hibernate.ddl-auto=create-drop
└── src/main/java/com/example/demo/
├── DemoApplication.java # @SpringBootApplication 启动入口
├── GreetingService.java # @Service
├── GreetingController.java # 构造器注入 GreetingService
├── Member.java # JPA @Entity
├── MemberRepository.java # JpaRepository
├── MemberController.java # /members CRUD(需认证)
├── MemberXmlMapper.java # MyBatis @Mapper(注解 SQL)
└── SecurityConfig.java # 过滤链 + BCrypt + 内存用户构建与运行:
mvn package -DskipTests # 产物 target/sb-demo-1.0.0.jar(54MB fat jar)
java -jar target/sb-demo-1.0.0.jar # 内嵌 Tomcat,约 5.7s 启动完成六、状态与参考
- 状态:已收录(2026-09-02,完成领域概览规划中 Spring Boot 的 4 项主题)。
- 运行环境:OpenJDK 17.0.20 / Maven 3.9.16 / Spring Boot 3.3.5 / H2(内存库,JDBC 驱动随工程)。数据访问与安全行为均以最小工程实测为准。
- 关联:Node.js / NestJS、数据库、鉴权认证、容器与部署、后端概览。
- 参考:Spring Boot 官方文档、Spring Security 参考文档、Spring Data JPA、MyBatis Spring Boot Starter。
下一步
- [ ] 沉淀 JWT 落地骨架:OncePerRequestFilter + SecurityContext + token 刷新
- [ ] MyBatis-Plus / QueryDSL 与 JPA Specification 的动态查询对比
- [ ] 事务边界与隔离级别在 Spring 中的传播行为(REQUIRED/REQUIRES_NEW)实测