【Spring6笔记】 - 10 - Spring IoC注解式开发
【Spring6笔记】 - 10 - Spring IoC 注解式开发
1、回顾注解(Annotation)
在深入 Spring 注解式开发之前,必须理解 Java 注解的本质:注解是一种标记,它本身不执行逻辑,而是由解释它的“工具”(如 Spring 容器或反射程序)通过读取标记来决定如何执行逻辑。
1.1 注解的定义与属性
注解的定义使用 @interface 关键字。在 Spring 源码中,几乎所有的核心注解(如 @Component)都包含以下几个关键要素:
1.1.1 元注解(Meta-Annotations)
元注解是“标注在注解上的注解”,元注解是 Java 定义的用于修饰其他注解的注解。在 Java java.lang.annotation 包下,最核心的元注解共有 4 个:
1. @Target (目标)
- 作用:限定被修饰的注解可以标注在源代码的哪些位置。
- 常用取值 (ElementType):
TYPE:类、接口(包括注解类型)或枚举声明。FIELD:字段、属性(包括枚举常量)。METHOD:方法。PARAMETER:形式参数。CONSTRUCTOR:构造函数。LOCAL_VARIABLE:局部变量。
- 示例:
@Target({ElementType.TYPE, ElementType.METHOD})表示该注解只能用在类和方法上。
2. @Retention (保留策略) —— Spring 开发最核心
- 作用:定义注解的生命周期,即注解被保留到什么时候。
- 取值 (RetentionPolicy):
SOURCE:仅存在于源码中,编译成.class文件时就被丢弃了(如@Override)。CLASS:默认值。注解记录在.class文件中,但运行期 JVM 无法读取。RUNTIME:Spring 几乎所有注解都用这个。注解记录在.class文件中,且运行期可以通过反射机制读取。
3. @Documented (文档化)
- 作用:指明该注解是否包含在生成的 JavaDoc 文档中。
- 说明:如果一个注解带了
@Documented,那么在使用该注解的类生成的 API 文档上,就能看到该注解的标记。
4. @Inherited (可继承性)
- 作用:标注该注解具有“继承性”。
- 说明:如果父类使用了被
@Inherited修饰的注解,那么子类如果没有使用任何注解,会自动继承父类的这个注解。- 注意:这只针对类的继承有效,对接口实现无效。
@Target(ElementType.TYPE) // 限制只能给类用
@Retention(RetentionPolicy.RUNTIME) // 必须是运行时,否则 Spring 读不到
@Documented // 建议加上,方便生成文档
@Inherited // 建议加上,让子类也能自动拥有组件属性
public @interface Component {
String value() default ""; // 建议给个默认值,防止报错
}
1.1.2 属性定义规则
- 格式:
类型 属性名();(例如String value();)。 - 特殊属性名
value:如果注解只有一个名为value的属性,使用时可以省略value =。 - 默认值:可以使用
default关键字指定默认值,如String value() default "";。
1.2 注解的使用语法
使用注解时,实际上是在为属性赋值。
// 完整写法
@Component(value = "userBean")
public class User { }
// 简化写法(仅限属性名为 value 且只有这一个属性赋值时)
@Component("userBean")
public class User { }
1.3 核心:通过反射读取注解(模拟 Spring IoC 扫描原理)
这是你笔记中最精华的部分。Spring 扫描包并实例化 Bean 的底层逻辑就是:遍历类 -> 判断是否有注解 -> 获取注解属性值 -> 反射实例化 -> 存入容器。
示例:模拟 Spring 扫描器
假设我们要扫描 com.zzz.bean 包,只有标注了 @Component 的类才会被放入 beanMap 容器中。
import java.io.File;
import java.net.URL;
import java.util.*;
public class AnnotationScanner {
public static void main(String[] args) throws Exception {
// 1. 模拟 Spring IoC 容器:存放 Bean 的 Map (ID -> Object)
Map<String, Object> beanMap = new HashMap<>();
// 2. 确定扫描范围
String packageName = "com.zzz.bean";
String path = packageName.replaceAll("\\.", "/");
// 3. 获取类加载器并定位资源
URL url = ClassLoader.getSystemClassLoader().getResource(path);
if (url == null) return;
File file = new File(url.getPath());
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
// 4. 转换文件名为类全路径名
String className = packageName + "." + f.getName().split("\\.")[0];
Class<?> clazz = Class.forName(className);
// 5. 判断类上是否标注了 @Component 注解
if (clazz.isAnnotationPresent(Component.class)) {
// 6. 获取注解对象并提取 value 属性(作为 BeanId)
Component component = clazz.getAnnotation(Component.class);
String beanId = component.value();
// 7. 反射实例化对象并存入容器
Object bean = clazz.getDeclaredConstructor().newInstance();
beanMap.put(beanId, bean);
}
}
}
// 验证结果
System.out.println("IoC 容器初始化完成:" + beanMap);
}
}
1.4 总结
- 注解的作用:在 Spring 6 中,注解彻底取代了繁琐的 XML 配置,使代码更简洁、高内聚。
- 反射是灵魂:如果没有反射机制,注解仅仅是注释。Spring 框架通过
Class.isAnnotationPresent()和Class.getAnnotation()实现了自动化的 Bean 管理。
2、声明 Bean 的注解
在 Spring IoC 容器中,如果想让一个类被自动实例化并管理,我们需要在该类上标注“声明式”注解。
2.1 四大核心注解一览
Spring 提供了四个主要的注解来标注组件,它们的功能是将当前类标识为 Spring Bean。
| 注解 | 适用层级 | 语义描述 |
|---|---|---|
@Component |
通用 | 泛指各种组件。当类不属于特定的分层(如工具类)时使用。 |
@Controller |
控制层 (Web) | 标注在 Spring MVC 的控制层类上,负责接收请求并返回响应。 |
@Service |
业务逻辑层 | 标注在 Service 类上,处理核心业务逻辑。 |
@Repository |
数据访问层 (DAO) | 标注在数据访问组件上,即直接操作数据库的类。 |
//@Component 注解源码
@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
//@Controller 注解源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
//@Service 注解源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
//@Repository 注解源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
2.2 源码深度解析:注解的“继承”与别名
@Controller、@Service、@Repository 的定义上都标注了 @Component。
关键发现:
- 元注解依赖:这三个注解本质上都是
@Component的衍生注解(也称为特化注解)。 @AliasFor的作用:源码中出现了@AliasFor(annotation = Component.class)。这意味着这些注解中的value属性实际上是给@Component的value属性传参。- 功能一致性:在目前的 Spring 版本中,这四个注解在“将类实例化并放入容器”这一基本功能上是完全等价的。
2.3 为什么要区分四个注解?
既然功能一样,为什么不全部使用 @Component?
-
代码可读性(语义化):
看到
@Service你就知道这是处理业务逻辑的,看到@Repository就知道这是操作数据库的。这符合“见名知意”的原则,方便团队协作和代码维护。 -
分支功能的扩展(后置处理):
虽然目前功能相似,但 Spring 为特定的注解提供了额外功能。例如:
@Repository:除了声明 Bean,它还会自动开启异常转换机制(PersistenceExceptionTranslationPostProcessor),将原生数据库异常转换为 Spring 的统一数据访问异常。@Controller:结合 Spring MVC 时,底层扫描器会专门寻找该注解来注册对应的 RequestMapping。
-
便于 AOP 切面编程:
我们可以轻松地编写切面,只拦截
@Service标注的类进行事务管理,而不影响其他的 Bean。
2.4 Bean 的命名规则(value 属性)
在四大注解中,都有一个 value 属性:
- 手动指定 ID:
@Component("myUser"),此时 Bean 在容器中的 ID 为"myUser"。 - 默认命名规则:如果省略
value属性,例如直接写@Component,Spring 会使用默认命名策略:- 类名首字母小写。
- 例如:类名是
UserService,默认 ID 是userService。 - 特殊情况:如果类名前两个字母都是大写(如
XUser),则默认 ID 就是类名本身。
虽然用哪个都可以,但绝对不要乱用!
在实际开发中,必须严格遵守分层规范:Web 层用
@Controller,业务层用@Service,持久层用@Repository。只有当一个类不属于这三层时,才考虑使用@Component。
3、Spring 注解的使用
要让 Spring 识别并管理标注了注解的类,主要分为两个步骤:第一步是添加依赖(引入 Spring 基础库),第二步是配置组件扫描。
3.1 引入依赖(Spring Context)
注解式开发依赖于 Spring 的核心容器。在 pom.xml 中,我们通常需要引入 spring-context 依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.2</version>
</dependency>
3.2 开启组件扫描(XML 配置方式)
在 Spring 6 彻底转向全注解开发之前,最经典的方式是在 spring.xml 文件中配置扫描路径。
3.2.1 引入 context 命名空间
在使用扫描标签前,必须在 XML 根节点中引入 context 相关的 schema。
3.2.2 配置 component-scan
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.zzz.bean" />
</beans>
3.3 扫描多个包的写法
如果你的项目中有多个包需要扫描,有以下几种写法:
- 逗号分隔法:
<context:component-scan base-package="com.zzz.service, com.zzz.dao" /> - 根包扫描法(最常用):
<context:component-scan base-package="com.zzz" />注意:扫描的包越精确,启动速度越快;扫描的范围越大,虽然省事,但扫描耗时更长。
4、选择性实例化 Bean
默认情况下,Spring 会扫描指定包下所有的四大注解并实例化。但通过配置 use-default-filters 属性以及内部的 filter 标签,我们可以实现精准控制。
4.1 核心属性:use-default-filters
- 作用:是否启用默认的过滤器。
- 默认值:
true。即默认扫描@Component,@Controller,@Service,@Repository。 - 设置为
false:所有声明 Bean 的注解全部失效,必须手动指定哪些注解生效。
4.2 解决方案一:包含过滤(include-filter)
如果我们希望只扫描特定的注解,而忽略其他的,可以使用这种组合:
- 设置
use-default-filters="false"(让默认规则全部失效)。 - 使用
<context:include-filter>指定要包含的注解。
XML 配置示例:
<context:component-scan base-package="com.zzz.bean" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
- 结果:只有标注了
@Repository和@Service的类会被实例化。标注了@Component或@Controller的类会被跳过。
4.3 解决方案二:排除过滤(exclude-filter)
如果我们希望扫描大部分注解,但排除掉个别注解,可以使用这种方案:
use-default-filters="true"(默认值,可省略)。- 使用
<context:exclude-filter>指定要排除的注解。
XML 配置示例:
<context:component-scan base-package="com.zzz">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
- 结果:
Book(@Component),BookDao(@Repository),BookService(@Service) 都会执行无参构造并实例化;而BookController不会被实例化。
4.4 实验验证与现象
@Test
public void testSpringChoose() throws Exception {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-choose.xml");
Book bookBean = applicationContext.getBean("book", Book.class);
System.out.println(bookBean);
BookDao bookDaoBean = applicationContext.getBean("bookDao", BookDao.class);
System.out.println(bookDaoBean);
BookService bookServiceBean = applicationContext.getBean("bookService", BookService.class);
System.out.println(bookServiceBean);
BookController bookControllerBean = applicationContext.getBean("bookController", BookController.class);
System.out.println(bookControllerBean);
}
@Component
public class Book {
public Book() {
System.out.println("Book()无参构造正在执行");
}
}
@Repository
public class BookDao {
public BookDao() {
System.out.println("BookDao()无参构造正在执行");
}
}
@Service
public class BookService {
public BookService() {
System.out.println("BookService()无参构造正在执行");
}
}
@Controller
public class BookController {
public BookController() {
System.out.println("BookController()无参构造正在执行");
}
}
<context:component-scan base-package="com.zzz">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
通过Junit测试 testSpringChoose 以及各个 Bean 的打印语句,可以观察到:
-
实例化时机:Spring 容器(
ApplicationContext)在初始化时,就会根据过滤规则调用符合条件的 Bean 的无参构造方法。 -
控制台输出:
-
如果使用了“排除 Controller”的配置,控制台只会打印:
Book()无参构造正在执行
BookDao()无参构造正在执行
BookService()无参构造正在执行
-
此时若尝试
context.getBean("bookController"),程序会抛出NoSuchBeanDefinitionException异常。

-
4.5 常见过滤类型 (Filter Type)
虽然最常用的是 annotation(注解类型),但 Spring 还支持其他过滤方式:
- annotation:按注解全类名匹配。
- assignable:按特定的类或接口类型匹配。
- aspectj:使用 AspectJ 表达式匹配。
- regex:使用正则表达式匹配。
在经典的 Spring + SpringMVC 项目中,为了防止重复创建对象:
- Spring 配置文件通常会使用
exclude-filter排除掉@Controller。- SpringMVC 配置文件通常使用
use-default-filters="false"并配合include-filter只包含@Controller。
5、负责注入的注解
Spring 提供了多种注解来处理依赖注入,主要分为两类:注入简单类型和注入引用类型(Bean)。
5.1 注入简单类型:@Value
@Value 用于将外部配置(如 .properties 文件)、字面量或 SpEL 表达式注入到 Bean 的属性中。
- 使用场景:注入数据库连接信息、配置参数等。
- 语法:
- 直接赋值:
@Value("张三") - 读取属性文件:
@Value("${user.name}") - SpEL 表达式:
@Value("#{2 * 10}")
- 直接赋值:
5.2 注入引用类型:@Autowired (Spring 官方)
这是 Spring 中最常用的注入方式。其核心特点是:默认按类型(byType)注入。
5.2.1 源码分析
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
// 默认 true:表示容器中必须存在该 Bean,否则抛出 BeanCreationException
// 设置 false:如果找不到 Bean,则注入 null,不报错
boolean required() default true;
}
5.2.2 注入原理
- Spring 扫描到
@Autowired。 - 在 IoC 容器中寻找类型匹配的 Bean。
- 如果只找到一个,直接注入。
- 如果找到多个(例如一个接口有多个实现类),Spring 会尝试**按名称(byName)**匹配(匹配属性名)。
- 如果名称也匹配不上,且没有其他辅助注解,则报错。
5.3 辅助注入:@Qualifier
当一个接口有多个实现类时,单纯靠 @Autowired 可能会导致歧义报错。此时必须配合 @Qualifier 明确指定 Bean 的 ID。
-
必须配合
@Autowired使用(不能单独使用)。 -
示例:
@Autowired @Qualifier("userDaoImplForMySQL") // 明确指定注入 ID 为此的 Bean private UserDao userDao;
5.4 资源注入:@Resource (JDK 标准)
@Resource 是由 JDK(Java 规范,javax.annotation 或 jakarta.annotation)提供的注解。Spring 对其提供了完美支持。
<!--@Resource注解的依赖-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
- 注入原理:默认按名称(byName)注入。
- 流程:
- 先找与属性名一致的 Bean。
- 如果按名称没找到,再尝试**按类型(byType)**匹配。
- 如果指定了
name属性(如@Resource(name="myBean")),则只按名称找,找不到直接报错。
@Autowired 与 @Resource 的区别:
| 特性 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架独有 | JDK 标准(JSR-250) |
| 注入方式 | 默认 byType | 默认 byName |
| 属性名匹配 | 类型匹配失败后才尝试名称 | 先匹配名称 |
| 适用位置 | 属性、构造器、方法、参数 | 属性、Setter 方法 |
6、全注解式开发(No-XML)
在全注解开发中,我们使用 配置类(Configuration Class) 来充当以前的 spring.xml。
6.1 核心注解:@Configuration 与 @ComponentScan
要实现全注解,必须依靠以下两个关键注解:
@Configuration:- 作用:标注在类上,告知 Spring 这是一个配置类,其角色等同于
beans.xml。 - 底层:被此注解标注的类本身也会被注册到 IoC 容器中。
- 作用:标注在类上,告知 Spring 这是一个配置类,其角色等同于
@ComponentScan:- 作用:对应 XML 中的
<context:component-scan>。 - 属性:
value或basePackages用于指定扫描的包路径。
- 作用:对应 XML 中的
配置类示例:
package com.zzz.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration // 代替 <beans> 根标签
@ComponentScan("com.zzz") // 代替 <context:component-scan base-package="com.zzz"/>
public class SpringConfig {
// 这里还可以通过 @Bean 注解来定义第三方库的对象(如 Druid 数据源)
}
6.2 容器启动方式的变化
由于不再有 XML 文件,我们不能再使用 ClassPathXmlApplicationContext 来启动容器,而必须使用 AnnotationConfigApplicationContext。
测试代码对比:
| 方式 | 容器启动代码 |
|---|---|
| XML 方式 | new ClassPathXmlApplicationContext("spring.xml"); |
| 全注解方式 | new AnnotationConfigApplicationContext(SpringConfig.class); |
测试代码示例:
@Test
public void testFullAnnotation() {
// 1. 通过配置类的 Class 对象创建容器
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 获取 Bean
BookService bookService = context.getBean("bookService", BookService.class);
// 3. 调用方法
bookService.save();
}
6.3 引入第三方 Bean:@Bean 注解
如果我们需要将第三方库(如 Druid、MyBatis 的 SqlSessionFactory)交给 Spring 管理,因为我们无法在人家的源码上打 @Component,所以需要在配置类中使用 @Bean。
@Configuration
@ComponentScan("com.zzz")
public class SpringConfig {
// @Bean 标注的方法,其返回值会自动纳入 Spring 容器管理
// 默认 Bean 的 ID 为方法名 "dataSource"
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/db");
ds.setUsername("root");
ds.setPassword("123456");
return ds;
}
}
测试代码:
@Test
public void testFullAnnotation() {
// 1. 通过配置类的 Class 对象创建容器
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 获取 Bean
DataSource dataSource = context.getBean("dataSource", DataSource.class);
System.out.println(dataSource);
}
测试结果:
6.4 总结:全注解开发的优势
- 类型安全:在 Java 代码中配置,如果有拼写错误,编译器会直接报错,而 XML 只有在运行时才会发现错误。
- 解耦配置:可以将不同功能的配置拆分成多个配置类(如
JdbcConfig、MvcConfig),通过@Import注解合并。 - 符合 Spring Boot 趋势:Spring Boot 的底层完全基于全注解开发,掌握了这一块,向 Spring Boot 过渡将毫无压力。
更多推荐




所有评论(0)