前言
IOC 是Spring的两大核心概念之一,它是一种思想,需要极其熟练的掌握。
今日摘录:
低能无聊的人太多。说他们勤勉,不过是因困为不会合理分配时间;说他们积极,不过是逃避其他困难工作而已。即便说工作只是生存手段,也没见他们有什么拿得出手的爱好或特长。我真是每天都在失望。低能无聊的人要是边不满边骂着自己的低能无聊却不愿做出任何改变,那就真是自寻烦恼自掘坟墓了。
——东野圭吾《变身》
1、IOC
IoC 是 Inversion of Control 的简写,译为“控制反转”,它不是一门技术,而是一种设计思想,是一个重要的面向对象编程法则,能够指导我们如何设计出松耦合、更优良的程序。
1.1、控制反转
-
控制反转不是技术,而是一种思想。
-
控制反转是为了降低程序耦合度,提高程序扩展力。
-
控制反转,反转的是什么?
-
将对象的创建权利交出去,交给第三方容器(IOC 容器)负责。
-
将对象和对象之间关系的维护权交出去,交给第三方容器负责。
-
-
控制反转这种思想如何实现呢?
-
DI(Dependency Injection):依赖注入
-
依赖注入(DI):
-
指Spring创建对象的过程中,将对象依赖属性通过配置进行注入
依赖注入常见的实现方式包括两种:
-
第一种:set注入
-
第二种:构造器注入
所以,IOC 就是一种控制反转的思想, 而 DI 是对 IOC的一种具体实现。
1.2、获取 Bean 的 3 种方式
其实也就是 getBean() 的三种传参方式,下面对我们上一节的 User 类进行测试。
<bean id="user" class="com.lyh.study.User"/>
1.2.1、getBean(String id)
@Test
public void testHelloWorld1(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
User user = (User)ac.getBean("user");
user.add();
}
1.2.2、 getBean(Class<? extends Object> requiredType)
因为传入的就是一个类,所以可以自动推断出返回的类型。
注意:这种方式的配置文件中属于该类(com.lyh.study.User)的 bean 只能有一个,这个很好理解,如果有多个,它怎么知道你到底要取哪一个 bean。
@Test
public void testHelloWorld1(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
User user = ac.getBean(User.class);
user.add();
}
1.2.3、getBean(String id,Class<? extends Object> requiredType)
这种方式同样指定了 bean 的类型,所以返回的直接就是该类型的 bean 对象。
@Test
public void testHelloWorld1(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
User user = ac.getBean("user",User.class);
user.add();
}
个人建议使用这种方式,不用咱们自己强转,代码看起来也比较直观。
1.3、依赖注入的 2 种方式
1.3.1、setter 注入
(1)创建 bean 目录,编写 Student 类:
package com.lyh.study.bean;
public class Student {
private Integer id;
private String name;
private Integer age;
private String sex;
public Student() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
注意:这里的 Bean 必须有 setter 方法,否则属性无法注入(idea 直接就会在配置文件中爆红);如果不设置 getter 方法,我们的私有属性无法获取(被public 修饰的属性可以通过 对象.属性(比如 student1.id) 直接获取,但是被 private 修饰的属性必须通过显示的方法(比如 student1.getId())来获取)。
(2)配置 bean 时给属性赋值:
<bean id="student1" class="com.lyh.study.bean.Student">
<property name="id" value="1001"/>
<property name="name" value="lyh"/>
<property name="age" value="20"/>
<property name="sex" value="男"/>
</bean>
(3)测试:
@Test
public void testStudent(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
Student student1 = ac.getBean("student1", Student.class);
System.out.println(student1);
}
(4)运行结果:
Student{id=1001, name='lyh', age=20, sex='男'}
1.3.2、构造器注入
(1)在我们的 JavaBean 中添加有参构造器:
public Student(Integer id, String name, Integer age, String sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}
(2)配置 bean
默认是按照我们构造器的顺序直接赋值:
<bean id="student2" class="com.lyh.study.bean.Student">
<constructor-arg value="1002"/>
<constructor-arg value="燕双鹰"/>
<constructor-arg value="28"/>
<constructor-arg value="男"/>
</bean>
也可以通过属性来指定属性的顺序:
- index 属性:从 0 开始(分别对应 id、name、age、sex)
- name 属性:直接指定参数名,然后通过 value 赋值
(3)测试
@Test
public void testStudent(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
Student student2 = ac.getBean("student2", Student.class);
System.out.println(student2);
}
(4)运行结果:
Student{id=1002, name='燕双鹰', age=28, sex='男'}
1.4、给对象类型的属性注入的 3 种方式
给 Student 类添加一个 Address 类型的属性:
(1)创建 Address 类
package com.lyh.study.bean;
public class Address {
private String province;
private String county;
private String village;
public Address(){
}
public Address(String province, String county, String village) {
this.province = province;
this.county = county;
this.village = village;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCounty() {
return county;
}
public void setCounty(String county) {
this.county = county;
}
public String getVillage() {
return village;
}
public void setVillage(String village) {
this.village = village;
}
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", county='" + county + '\'' +
", village='" + village + '\'' +
'}';
}
}
完了给我们的 Student 类添加一个 Adress 类型的属性,并设置 getter 和 setter 方法。
1.4.1、外部引用 Bean
第一种方式:我们通过外部 Bean 引用来给我们的属性赋值:
(1)配置文件添加(使用 ref 属性来引用其它 bean):
<bean id="address1" class="com.lyh.study.bean.Address">
<property name="province" value="山西省"/>
<property name="county" value="天水县"/>
<property name="village" value="英雄村"/>
</bean>
<bean id="student3" class="com.lyh.study.bean.Student">
<property name="id" value="1003"/>
<property name="name" value="姜伯约"/>
<property name="age" value="26"/>
<property name="sex" value="男"/>
<property name="address" ref="address1"/>
</bean>
(2)测试:
@Test
public void testStudent(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
Student student3 = ac.getBean("student3", Student.class);
System.out.println(student3);
System.out.println(student3.getAddress());
}
(3)运行结果:
Student{id=1003, name='姜伯约', age=26, sex='男'}
Address{province='山西省', county='天水县', village='英雄村'}
1.4.2、内部 Bean
(1)配置文件:
<bean id="student4" class="com.lyh.study.bean.Student">
<property name="id" value="1004"/>
<property name="name" value="李大喜"/>
<property name="age" value="20"/>
<property name="sex" value="男"/>
<property name="address">
<!-- 内部bean不能给外部使用,所以可以省略 id 属性 -->
<bean class="com.lyh.study.bean.Address">
<property name="province" value="山西省"/>
<property name="county" value="英雄县"/>
<property name="village" value="农民村"/>
</bean>
</property>
</bean>
这里省略测试,和上面的结果是一样的。
1.4.3、级联属性赋值
使用外部 bean 重新给属性赋值:
<bean id="address1" class="com.lyh.study.bean.Address">
<property name="province" value="山西省"/>
<property name="county" value="天水县"/>
<property name="village" value="英雄村"/>
</bean>
<bean id="student5" class="com.lyh.study.bean.Student">
<property name="id" value="1005"/>
<property name="name" value="狄仁杰"/>
<property name="age" value="28"/>
<property name="sex" value="男"/>
<property name="address" ref="address1"/>
<property name="address.province" value="山西省"/>
<property name="address.county" value="杨柳县"/>
<property name="address.village" value="大柳树村"/>
</bean>
1.5、特殊类型属性的注入
我们的对象类型还可能是其它类型,比如数组、集合类型。这些特殊类型属性的注入无非就是配置文件的写法变化罢了。
1.5.1、数组类型
我们给上面的 Student 类添加一个 Int[] 类型的 hobbies 属性,记得添加 setter (没有 setter 方法就无法注入)和 getter 方法。
<bean id="student6" class="com.lyh.study.bean.Student">
<property name="id" value="1006"/>
<property name="name" value="狄如燕"/>
<property name="age" value="19"/>
<property name="sex" value="女"/>
<property name="address" ref="address1"/>
<property name="hobbies">
<array>
<value>唱歌</value>
<value>舞剑</value>
</array>
</property>
</bean>
1.5.2、集合类型
(1)List 类型
我们给上面的 Student 类添加一个 List<Student> 类型的 students 属性并添加 setter 和 getter 方法。
<bean id="student7" class="com.lyh.study.bean.Student">
<property name="id" value="1006"/>
<property name="name" value="狄如燕"/>
<property name="age" value="19"/>
<property name="sex" value="女"/>
<property name="address" ref="address1"/>
<property name="students">
<list>
<ref bean="student4"/>
<ref bean="student5"/>
<ref bean="student6"/>
</list>
</property>
</bean>
注意:如果是 Set 集合,只需要将其中的list标签改为set标签即可。
(2)Map 类型
我们给上面的 Student 类添加一个 Map<String,Student> 类型的 studentMap 属性并添加 setter 和 getter 方法。
<bean id="student8" class="com.lyh.study.bean.Student">
<property name="id" value="1006"/>
<property name="name" value="狄如燕"/>
<property name="age" value="19"/>
<property name="sex" value="女"/>
<property name="address" ref="address1"/>
<property name="studentMap">
<map>
<entry>
<key>
<value>1001</value>
</key>
<!-- 如果value是基本类型,直接使用<value>标签即可 -->
<ref bean="student1"/>
</entry>
<entry>
<key>
<value>1002</value>
</key>
<ref bean="student2"/>
</entry>
</map>
</property>
</bean>
1.5.3、通过 <util:> 标签实现集合类型属性的注入
要使用 util 标签就必须先引入它,在配置文件头部需要加入以下内容:
使用 util 标签实现 Map 集合类型注入:
<bean id="student8" class="com.lyh.study.bean.Student">
<property name="id" value="1008"/>
<property name="name" value="狄如燕"/>
<property name="age" value="19"/>
<property name="sex" value="女"/>
<property name="address" ref="address1"/>
<property name="studentMap" ref="map"/>
</bean>
<util:map id="map">
<entry>
<key>
<value>1001</value>
</key>
<ref bean="student1"/>
</entry>
<entry>
<key>
<value>1002</value>
</key>
<ref bean="student2"/>
</entry>
</util:map>
可以看到,效果和上面直接通过 <map> 标签注入是一样的,这个 <util:map> 标签就相当于一个 Map 对象。
1.6、P 命名空间注入
命名空间是啥东西,其实就是我们 Spring 配置文件的头部那些带有 xlms: xxx 的部分(比如 xmls:util 就是 util 命名空间),所以 P 命名空间注入其实就是加这么一行:
有啥用呢?其实也就是用来简化配置文件的代码量:
<bean id="student10" class="com.lyh.study.bean.Student" p:id="1010" p:name="武则天" p:age="58" p:address-ref="address1" p:studentMap-ref="map"/>
可以看到,引入 p命名空间后,我们的属性直接变成了 bean 标签的一个属性值 p:属性名,引用类型的属性也可以通过 p:属性名-ref 的方式来引用。
1.7、引入外部属性文件
我们经常需要把一些常用但是又经常需要修改的类写入到 Spring 配置文件(比如 MySQL 工具类),而这些 Bean 的属性值的修改就需要通过外部属性文件来进行注入了,这样更加灵活,方便维护。
(1)需求:
把一些特定的固定值,放到一个特定的外部文件中去(比如 db.properties),在 Spring 配置文件中进行引入,这样我们要进行修改时,只需要修改外部文件,而不需要修改代码。
(2)导入依赖:
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- 数据源-连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
(3)创建外部属性文件 db.properties
jdbc.user=root
jdbc.password=Yan1029.
jdbc.url=jdbc:mysql://localhost:3306/flink?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
1.7.1、引入属性文件
(1)引入 context 名称空间:
(2)引入外部属性文件(db.properties)
<context:property-placeholder location="db.properties"/>
(3)配置连接池对应的 Bean
这个 Bean druid已经帮我们实现了,相当于一个工具类,我们都不用自己实现,只需要让 Spring 帮我管理即可。
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
(4)测试:
@Test
public void testDataSource() throws SQLException {
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
DataSource druidDataSource = ac.getBean("druidDataSource", DataSource.class);
Connection connection = druidDataSource.getConnection();
System.out.println(connection);
connection.close();
}
(5)运行结果:
com.mysql.cj.jdbc.ConnectionImpl@5b56b654
1.8、Bean 的作用域
在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围:
取值 | 含义 | 创建对象的时机 |
---|---|---|
singleton(默认) | 在IOC容器中,这个bean的对象始终为单实例 | IOC容器初始化时 |
prototype | 这个bean在IOC容器中有多个实例 | 获取bean时 |
也就是我上一节说的,单例对象和多例对象的区别。
如果是在 WebApplicationContext 环境下还会有另外几个作用域(但不常用):
取值 | 含义 |
---|---|
request | 在一个请求范围内有效 |
session | 在一个会话范围内有效 |
我是应该用不上了。
这里说一下单例模式(singleton)和非单例模式(prototype)的区别:
- 单例模式下,每次 getBean 都会返回同一个对象(在内存中的地址相同,可以用 == 进行测试)
- 非单例模式下,每次 getBena 都会创建一个新的对象,尽管我们在配置文件中设置的它们的属性是一样的,但是它们指向的是不同的内存地址。
1.9、Bean 的生命周期
- bean对象创建(调用无参构造器)
- 给bean对象设置属性(调用我们自己写的 setter 方法)
- bean的后置处理器(初始化之前)
- bean对象初始化(需在配置bean时指定初始化方法)
- bean的后置处理器(初始化之后)
- bean对象就绪可以使用
- bean对象销毁(需在配置bean时指定销毁方法)
- IOC容器关闭
1.9.1、初始化和销毁方法
上面的 Bean 对象的初始化方法和销毁方法是我们自己实现的,然后通过给 <bean> 标签添加属性来实现:
<!-- 使用init-method属性指定初始化方法 -->
<!-- 使用destroy-method属性指定销毁方法 -->
<bean class="com.lyh.study.bean.User" scope="prototype" init-method="initMethod" destroy-method="destroyMethod">
<property name="id" value="1001"></property>
<property name="name" value="朱重八"></property>
<property name="age" value="18"></property>
<property name="sex" value="男"></property>
</bean>
1.8.2、后置处理器
bean 的后置处理器会在生命周期的初始化前后添加额外的操作,需要实现 BeanPostProcessor 接口,且配置到IOC容器中。
需要注意的是:bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
package com.lyh.study;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class MyBeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 初始化之前的处理代码
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 初始化之后的处理代码
return bean;
}
}
在 IOC 容器内配置后置处理器(放进去就行了):
<!-- bean的后置处理器要放入IOC容器才能生效 -->
<bean id="myBeanProcessor" class="com.lyh.study.MyBeanProcessor"/>
1.10、FactoryBean
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean 类型的 bean(FactoryBean 是一个接口,所以这里说 FactoryBean类型的Bean 指的其实是 它的实现类),在获取 bean 的时候得到的并不是 class 属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
其实我们整合 Spring + Mybatis 时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
(1)实现 FactoryBean 接口:
package com.lyh.study;
import com.lyh.study.bean.Student;
import org.springframework.beans.factory.FactoryBean;
public class StudentFactoryBean implements FactoryBean<Student> {
@Override
public Student getObject() throws Exception {
return new Student();
}
@Override
public Class<?> getObjectType() {
return Student.class;
}
}
(2)添加到 IOC 容器:
<bean id="studentFactoryBean" class="com.lyh.study.StudentFactoryBean"/>
(3)测试:
@Test
public void testBeanFactory(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
// 转换出来的不是 StudentBeanFactory 对象,而是 Student 对象
Student studentBeanFactory = (Student) ac.getBean("studentFactoryBean");
System.out.println(studentBeanFactory);
}
(4)运行结果:
Student{id=null, name='null', age=null, sex='null'}
当 Spring 容器遇到一个实现了 FactoryBean 接口的 Bean 时,它不会直接实例化这个 Bean,而是会调用该 Bean 的 getObject() 方法来获取对象。这样,我们就可以在 getObject() 方法中编写自定义的对象创建逻辑,从而实现与第三方框架的整合。
1.11、基于 XML 的自动装配
所谓自动装配其实就是为了减少我们配置 Spring 配置文件的工作量,比较一个 JavaBean 如果有很多属性的话,我们自己一个一个去配置添加属性太复杂了,所以就有了自动装配这个概念。
自动装配有两种方式:通过属性类型来自动装配(byType),通过属性名来自动装配(byName)。
- byType:根据类型匹配IOC容器中的某个兼容类型的bean,为属性自动赋值
- 如果在IOC中,没有任何一个兼容类型的bean能够为属性赋值,则该属性不装配,即值为默认值 null
- 如果在IOC中,有多个兼容类型的bean能够为属性赋值,则抛出异常NoUniqueBeanDefinitionException
- byName:将自动装配的属性的属性名,作为bean的id在IOC容器中匹配相对应的bean进行赋值
这里我们写一个测试模拟我们开发的一个的场景,controller 负责响应通过 service 来实现,service 会调用 dao 层来实现持久化。
(1)编写 Service 层代码:
package com.lyh.study.service;
public interface UserService {
void addUser();
}
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
(2)编写 Dao 层代码 :
package com.lyh.study.dao;
public interface UserDao {
void addUser();
}
package com.lyh.study.dao;
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
System.out.println("添加成功");
}
}
(3)编写 controller 层代码:
package com.lyh.study.controller;
import com.lyh.study.service.UserService;
public class UserController {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void addUser(){
userService.addUser();
}
}
1.11.1、byType 自动装配
<bean id="userController" class="com.lyh.study.controller.UserController" autowire="byType"/>
<bean id="userService" class="com.lyh.study.service.UserServiceImpl" autowire="byType"/>
<bean id="userDao" class="com.lyh.study.dao.UserDaoImpl"/>
1.11.2、byName 自动装配
<bean id="userController" class="com.lyh.study.controller.UserController" autowire="byName"/>
<bean id="userService" class="com.lyh.study.service.UserServiceImpl" autowire="byName"/>
<bean id="userDao" class="com.lyh.study.dao.UserDaoImpl"/>
测试:
@Test
public void testAutoWireByType(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
UserController userController = ac.getBean(UserController.class);
userController.addUser();
}
运行结果:
addUser() 方法执行
添加成功
2、基于注解管理 Bean
除了上面的直接手动配置 Spring 配置文件以外,我们实际用的更多应该就是使用注解注入了。
2.1、开启扫描组件
Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。
也就是在 Spring 配置文件中加这么一行:
<context:component-scan base-package="com.lyh.study"/>
注意:在使用 context:component-scan 元素开启自动扫描功能前,首先需要在 XML 配置的一级标签 <beans> 中添加 context 相关的约束。
2.1.1、默认扫描方式
<context:component-scan base-package="com.lyh.study"/>
也就是我们上面演示的,它会扫描 com.lyh.study 包下所有被 @Component 注解标注的类并帮我们注册到 IOC 容器中管理。
2.1.2、指定要排除的组件
<context:exclude>标签
<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.lyh.study" use-default-filters="false">
<!--
type:设置包含的依据
type="annotation",根据注解排除,expression中设置要排除的注解的全类名
type="assignable",根据类型排除,expression中设置要排除的类型的全类名
-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="assignable" expression="com.lyh.study.controller.UserController"/>
</context:component-scan>
2.1.3、仅扫描指定组件
<context:include> 标签
<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.lyh.study" use-default-filters="false">
<!--
type:设置排除的依据
type="annotation",根据注解排除,expression中设置要排除的注解的全类名
type="assignable",根据类型排除,expression中设置要排除的类型的全类名
-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="assignable" expression="com.lyh.study.controller.UserController"/>
</context:component-scan>
2.2、使用注解定义 Bean
Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。
注解 | 说明 |
---|---|
@Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 |
@Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
2.3、@Autowired 注入
单独使用 @Autowired注解时,默认根据类型装配(byType)。
2.3.1、属性注入
上面 2.1 中我们的 UserController 中有一个属性是 UserService 类型的对象,而 UserService 这个类当中也有一个类型为 UserDao 的属性;所以我们当时必须提供 setter 方法,因为它是是通过 setter 方法进行注入的;但是现在使用注解开发我们就可以省去 setter 方法:
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
System.out.println("添加成功");
}
}
package com.lyh.study.controller;
import com.lyh.study.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private UserService userService;
public void addUser(){
userService.addUser();
}
}
这次我们使用了注解 @Service、Repository、Controller 分别标注了我们的 UserController、UserDao 和 UserController ,而且即使它们的 Bean 中包含了一些属性,我们并没有提供 setter 方法,因为使用注解开发时,不需要给 Bean 提供 setter 方法。
测试:
@Test
public void testAutoWireByType(){
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
UserController userController = ac.getBean("userController",UserController.class);
userController.addUser();
}
运行结果:
addUser() 方法执行
添加成功
2.3.2、set 注入
上面的属性注入中,我们把 @Autowired 这个注解标注在了属性上,这种方式不需要我们实现属性的 setter 方法;而 set 注入是直接把 @Autowired 这个注解标注在方法上:
package com.lyh.study.controller;
import com.lyh.study.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void addUser(){
userService.addUser();
}
}
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
这里不再测试运行结果了,效果和属性注入是一样的。个人推荐这种方式,尽量养成一个给属性添加 setter 方法的好习惯,而且属性注入 Idea 会给一个警告提示,虽然也用起来没问题,但是强迫症实在受不了。
2.3.3、构造方法注入
和上面两种方法的注入方式差不多,就是把 @Autowired 这个注解标注在了构造器上,这种方式同样不需要给属性提供 setter 方法。
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import com.lyh.study.dao.UserDaoImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao){
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
package com.lyh.study.controller;
import com.lyh.study.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
public void addUser(){
userService.addUser();
}
}
效果和上面一致,不做演示。
2.3.4、形参注入
把 @Autowired 标注在形参上。
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import com.lyh.study.dao.UserDaoImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(@Autowired UserDao userDao){
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
package com.lyh.study.controller;
import com.lyh.study.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
public UserController(@Autowired UserService userService){
this.userService = userService;
}
public void addUser(){
userService.addUser();
}
}
同样不做测试。
2.4.5、只有一个构造函数,无注解
当我们的 Bean 只有一个构造函数时,可以不需要注解。我们也可以从 Idea 的只能提示中看出来,当只有一个构造函数时它会自动被 Spring IOC 容器所管理(当然,我们的 Service 类上的 @Service 还是得有的)。
注意:再添加一个无参构造函数就失效了(有参构造和无参构造只能有一个)!!!
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import com.lyh.study.dao.UserDaoImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao){
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
package com.lyh.study.controller;
import com.lyh.study.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
public UserController(UserService userService){
this.userService = userService;
}
public void addUser(){
userService.addUser();
}
}
2.4.6、@Autowired注解和@Qualifier注解联合
假设我们需要扩展一个名为 UserOracleDaoImpl 的类,用来把数据持久化到 Oracle 数据库中。
package com.lyh.study.dao;
import org.springframework.stereotype.Repository;
@Repository
public class UserOracleDaoImpl implements UserDao{
@Override
public void addUser() {
System.out.println("用户被添加到 Oracle 数据库中");
}
}
当我们进行测试的时候,会发现报错,甚至 Idea 自动会检测到异常不允许编译通过。原因就是 UerDao 类型的类 = 2 ,根本原因其实就是我们使用的 byType 自动装配,它要求我们的 IOC 容器中只能包含一个这种类型的 Bean。
怎么解决呢?其实很简单,换 byName 自动装配就 OK 了,也就是把 @Autowired注解和@Qualifier注解联合使用(标注在 setter 方法或者 属性上都是可以的):
因为我们上面的 UserDaoImpl 和 UserOracleDaoImpl 都已经被 @Respository 标注过了,所以它俩已经都被注册进了我们的 IOC 容器中,切换不同的实现类只需要修改 id :
使用 userDaoImpl :
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
@Qualifier("userDaoImpl")
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
要换用 userOrcaleDaoImpl 直接修改 id 即可:
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired
@Qualifier("userOracleDaoImpl")
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
总结
-
@Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
-
当带参数的构造方法只有一个,@Autowired注解可以省略。
-
@Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
2.4、@Resource 注入
和上面的 @Autowired 一样,@Resource 也可以完成属性的注入。
-
@Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
-
@Autowired注解是Spring框架自己的。
-
@Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型byType装配。
-
@Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。
-
@Resource注解用在属性上、setter方法上。
-
@Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上。
@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。】
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
2.4.1、根据 name 注入
也就是根据 @Resource(name = "xxx") 的方式来找到对应的引用 Bean
package com.lyh.study.dao;
import org.springframework.stereotype.Repository;
@Repository("myUserDao")
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
System.out.println("添加成功");
}
}
需要和引用的类指定的 id 对应上(要引用上面的实现类就得指定 name = "myUserDao"):
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "myUserDao")
private UserDao userDao;
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
测试通过。
2.4.2、未知 name 注入
这次这里的 @Resource 不指定name,如果没有指定 name 它首先会去 IOC 容器中找 id = 该属性名(也就是 userDao)的 Bean,如果没有,再按照 byType 去找。
注意:这里我们定义的属性名 userDao 实际 IOC 容器中并没有 id = "userDao" 这么个 Bean,所以,它会继续按照类型去找,但是如果我们有两个 Bean 它们都实现了 UserDao 接口,那么它就会报错。
package com.lyh.study.service;
import com.lyh.study.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
@Override
public void addUser() {
System.out.println("addUser() 方法执行");
userDao.addUser();
}
}
测试通过。
2.5、Spring 全注解开发
全注解开发就是不再使用spring配置文件了,写一个配置类来代替配置文件,这个配置类需要被 @Configuration 注解标注。
编写一个配置类,扫描 "com.lyh.study" 包下所有被 IOC 容器管理注的类:
package com.lyh.study.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.lyh.study")
public class SpringConfig01 {
}
测试:
这里获取上下文对象使用的是 AnnotationConfigApplicationContext ,之前我们用的是 ClassPathXmlApplicationContext ,需要注意一下。
@Test
public void testAllAnnotation(){
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig01.class);
UserController userController = ac.getBean("userController", UserController.class);
userController.addUser();
}
测试成功。