基于注解的 IoC
# 40.基于注解的 IoC
本文就来讲解下,如何使用注解来完成 IoC。
# 前言
学习基于注解的 IoC 配置之前,大家脑海里首先得有一个认知,即注解配置和 项目 XML 配置要实现的功能都是一样的,都是要降低程序间的耦合,只是配置的形式不一样。
关于实际的开发中到底使用 XML 还是注解,每家公司有着不同的使用习惯。所以这两种配置方式我们都需要掌握。
我们在讲解注解配置时,采用上一篇博客的案例,把 Spring 的 XML 配置内容改为使用注解逐步实现。
注解的优势:配置简单,维护方便(我们找到类,就相当于找到了对应的配置)。
XML 的优势:修改时,不用改源码。不涉及重新编译和部署
# 调整代码
我们将界面层的代码还原成最开始的模样:
public class Client {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService)ac.getBean("accountService");
System.out.println(as);
}
}
2
3
4
5
6
7
调整 service 实现类,去掉构造函数:
public class AccountServiceImpl implements IAccountService {
private String name;
private Integer age;
private Date birthday;
@Override
public void saveAccount() {
System.out.println("service中的saveAccount方法执行了");
}
}
2
3
4
5
6
7
8
9
10
11
12
接下来我们可以删除 bean.xml 中的配置的所有内容。
# 注解的分类
我们可以根据曾经的基于 XML 的配置:
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"
scope="" init-method="" destroy-method="">
<property name="" value="" | ref=""></property>
</bean>
2
3
4
将注解分为 4 类:
- 用于创建对象的:作用和在 XML 配置文件中,编写一个
<bean>
标签实现的功能是一样的 - 用于注入数据的:作用和在 XML 配置文件中,bean 标签中写一个
<property>
标签的作用是一样的 - 用于改变作用范围的:作用和在 bean 标签中,使用
scope
属性实现的功能是一样的 - 和生命周期相关:作用和在 bean 标签中,使用
init-method
和destroy-methode
的作用是一样的
# 用于创建对象的注解
# @Component 注解
@Component:用于把当前类对象存入 spring 容器中,相当于:<bean id = "" class = "">
属性:value 用于指定 bean 的 id。当我们不写时,它的默认值是当前类名,首字母改小写。
书写位置:在类的定义前。由于我们是写在类的前面,所以 class 属性就不用写了
示例:
@Component("accountService")
public class AccountServiceImpl implements IAccountService {
private String name;
private Integer age;
private Date birthday;
@Override
public void saveAccount() {
System.out.println("service中的saveAccount方法执行了");
}
}
2
3
4
5
6
7
8
9
10
11
12
# 在 bean.xml 中配置注解所在包
使用注解后,需要在配置文件里标明哪里用了注解,约束也要设置,否则是无法运行的。因此我们需要告知 Spring,在创建容器时要扫描的包。
配置所需要的标签,不在 beans 的名称空间和约束中,而是一个名称为 context 名称空间和约束中。
寻找约束:和昨天一样,我们可以在文档中寻找约束,在 Core 模块中搜索 xmlns:context
:
我们复制该约束,然后加上 context
标签,并加上包名:
<?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.peterjxl"/>
</beans>
2
3
4
5
6
7
8
9
10
11
再次运行 Client,可以看到能正常运行,说明使用注解改造成功了。
# @Component 的衍生注解
除了@Component 注解可以用来创建 bean 之外,还有 3 个标签可以表示创建 bean:
- @Controller:一般用在表现层
- @Service:一般用在业务层
- @Repository:一般用在持久层
他们三个注解都是@Component 的衍生注解,他们的作用及属性都是一模一样的,只不过提供了更加明确的语义化。
改造:我们将 service 层的代码改为@Service 注解
@Service("accountService")
public class AccountServiceImpl implements IAccountService
2
同理,在 Dao 实现类使用@Repository
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Override
public void saveAccount() {
System.out.println("保存了账户");
}
}
2
3
4
5
6
7
试着打印和获取:
public class Client {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService)ac.getBean("accountService");
System.out.println(as);
IAccountDao accountDao = ac.getBean("accountDao", IAccountDao.class);
System.out.println(accountDao);
}
}
2
3
4
5
6
7
8
9
10
11
运行结果:可以正常创建对象
com.peterjxl.service.impl.AccountServiceImpl@aecb35a
com.peterjxl.dao.impl.AccountDaoImpl@5fcd892a
2
# 用于注入数据的注解
接下来我们试着在 service 层中,调用 dao 层的方法,此时我们需要注入 dao 对象
# @Autowired
@Autowired 的作用,就和在 XML 配置文件中的 bean 标签中写一个 <property>
标签的作用是一样的。它会自动按照类型注入,只要容器中有唯一的一个 bean 对象类型和要注入的变量类型匹配(及时是实现类),就可以注入成功。此时会跳过 beanID,直接寻找容器中,哪个对象的类型匹配。
出现位置:可以是变量上,也可以是方法上
我们可以试着在 service 层中,调用 dao 层的方法,并使用@Autowired 注解:
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao;
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
然后我们试着调用,是可以正常运行的:
public class Client {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService)ac.getBean("accountService");
as.saveAccount();
}
}
2
3
4
5
6
7
# @Autowired 的细节
- 在使用注解注入时,set 方法就不是必须的了。
- 如果 IoC 容器中没有任何 bean 的类型和要注入的变量类型匹配,则报错。
- 如果 IoC 容器中有多个类型匹配时,首先按照类型选出所有匹配的,然后根据变量名和 BeanID 是否一样,都不一样则报错。
我们可以演示下:修改 IAccountDao 的变量名为 dao:
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao dao;
@Override
public void saveAccount() {
dao.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
然后复制一个新的类:AccountDaoImpl2
,并增加构造方法、修改 beanID:
@Repository("accountDao2")
public class AccountDaoImpl2 implements IAccountDao {
public AccountDaoImpl2() {
System.out.println("AccountDaoImpl2对象创建了");
}
@Override
public void saveAccount() {
System.out.println("保存了账户2222");
}
}
2
3
4
5
6
7
8
9
10
11
再创建一个新的类:AccountDaoImpl3
,,并增加构造方法、修改 beanID:
@Repository("accountDao3")
public class AccountDaoImpl3 implements IAccountDao {
public AccountDaoImpl3() {
System.out.println("AccountDaoImpl3对象创建了");
}
@Override
public void saveAccount() {
System.out.println("保存了账户3333");
}
}
2
3
4
5
6
7
8
9
10
11
此时我们运行 Client,是会报错的:
No qualifying bean of type 'com.peterjxl.dao.IAccountDao' available: expected single matching bean but found 3: accountDao,accountDao2,accountDao3
大意:期望能找到一个匹配的,但是找到了 3 个。
如果我们修改 service 层中,变量名为 accountDao2:
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
private IAccountDao accountDao2;
@Override
public void saveAccount() {
accountDao2.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
此时是可以正常运行的,并且输出语句为:保存了账户 2222
也就是说,自动注入的逻辑
- 寻找容器中所有匹配的对象
- 如果有一个,则直接使用
- 如果有多个,则根据变量名和 key 名是否一样
# @Qualifier
作用:在按照类中注入的基础之上,再按照名称注入。它在给类成员注入时不能单独使用,需要和 Autowired 配合独立使用,但是在给方法参数注入时可以独立使用。
属性:value,用于指定注入 bean 的 id。
例如,我们可以指定注入 accountDao3 对象:
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
@Autowired
@Qualifier("accountDao3")
private IAccountDao accountDao2;
@Override
public void saveAccount() {
accountDao2.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
12
此时的运行结果:保存了账户 3333,也就是调用了 accountDao3 的方法。
# @Resource
@Autowired 自动注入的话得变量名和 beanid 一致,而@Qualifier 又不能独立使用,有没一个一步到位的注解呢?有的,那就是@Resource。
作用:直接按照 bean 的 id 注入。它可以独立使用
属性:name,用于指定 bean 的 id。
@Resource(name = "accountDao2")
private IAccountDao accountDao2;
2
# 基本类型的注入@Value
以上三个注解,都只能注入其他 bean 类型的数据,集合类型的注入只能通过 XML 来实现。基本类型和 String 类型无法使用上述注解实现的,我们得用@Value 注解。
作用:用于注入基本类型和 String 类型的数据
属性:value 用于指定数据的值。可使用 Spring 中 SpEL(也就是 Spring 的 el 表达式,SpEL 的写法:${表达式}
,JSP、Mybatis 也有 EL 表达式)
# 用于改变作用范围的注解@Scope
作用:用于指定 bean 的作用范围,就和在 bean 标签中使用 scope 属性实现的功能是一样的,默认是单例
属性:value 指定范围的取值。常用取值:singleton prototype
@Scope("prototype")
# 和生命周期相关的注解(了解即可)
作用:就和在 bean 标签中使用 init-method 和 destroy-methode 的作用是一样的
@PreDestroy 作用:用于指定销毁方法
@PostConstruct 作用:用于指定初始化方法
@PostConstruct
public void init(){
System.out.println("初始化方法执行了");
}
@PreDestroy
public void destroy(){
System.out.println("销毁方法执行了");
}
2
3
4
5
6
7
8
9
# 源码
本项目已将源码上传到 GitHub (opens new window) 和 Gitee (opens new window) 上。并且创建了分支 demo4,读者可以通过切换分支来查看本文的示例代码。