Skip to content

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 / @ControllerWeb 层,同时注册为 Bean接口入口

依赖注入三种姿势:

java
// 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; }  // ← 容器自动传入
    // ...
}
java
// 2. Setter 注入:可选依赖、需要运行时替换时用
// 3. 字段注入(@Autowired 直接打在字段上):最省事但难测、依赖隐式——新代码避免

Spring 官方与多数团队规范都要求构造器注入:能发现"漏依赖"(构造参数缺失直接启动失败而非运行时 NPE)、字段可 final、测试时手动 new 即可注入 mock。

实测:注入链路与启动装配

容器在启动阶段一次性完成「Bean 实例化 → 依赖解析 → 注入 → 路由映射」,最小工程实测启动日志:

text
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(数据)

注解作用示例
@RestControllerJSON API:返回值直接序列化为响应体类上
@GetMapping / @PostMapping方法与 HTTP 方法+路径绑定@GetMapping("/members/{id}")
@PathVariable取路径段{id}Long id
@RequestParam取查询串?name=xString name
@RequestBody反序列化请求体为对象@RequestBody Member body
@Valid + DTO参数校验(配合 starter-validation)@Valid @RequestBody
java
@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()));
    }
}

统一异常处理(返回结构化错误而非默认错误页):

java
@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);
    }
}

实测:路由行为

bash
$ 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 全都有;按方法名派生查询(findByNamefindByAgeGreaterThan)自动翻译成 SQL。

java
@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 协作场景。

java
@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 表:

text
[data] JPA   member 数量 = 3     ← JPA save 了 2 条 + MyBatis insert 了 1 条
[data] MyBatis member 数量 = 3   ← 两条链路读同一张表,结果一致
[data] JPA 注入示例: repository 实例 = MemberRepository

GET /members(认证后)返回:

json
[{"id":1,"name":"jpa-member"},{"id":2,"name":"jpa-member-2"},{"id":3,"name":"mybatis-member"}]

选型对比

维度Spring Data JPAMyBatis
思维模型面向对象,框架生成 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 都在这条链上拦截)。

java
@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 而不是等值):

text
[security] BCrypt hash of 'secret' = $2a$10$SWy5v/QR1kWZWUGKBwaFxeSYUi/0hb24.mNno6y9yXmCWKqb65ZE6
请求凭据结果
GET /greet(白名单)200 Hello, Spring!
GET /members401 Unauthorized
GET /membersadmin:secret200 JSON 成员列表
GET /membersadmin:wrong401 Unauthorized
POST /members(带 Basic)admin:secret201/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;
  • 前端跨域调接口要配 CORScors() 结合 @CrossOriginCorsConfigurationSource),否则浏览器拦截响应;
  • 不要用 {noop} 前缀存明文密码(Spring 允许但会警告),上线前全量迁移为 BCrypt。

五、最小工程结构参考(本文实测工程)

text
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 + 内存用户

构建与运行:

bash
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 启动完成

六、状态与参考

下一步

  • [ ] 沉淀 JWT 落地骨架:OncePerRequestFilter + SecurityContext + token 刷新
  • [ ] MyBatis-Plus / QueryDSL 与 JPA Specification 的动态查询对比
  • [ ] 事务边界与隔离级别在 Spring 中的传播行为(REQUIRED/REQUIRES_NEW)实测

基于 VitePress 构建 · 内容以知识共享方式沉淀