Spring IoC(控制反转)

前言

通过Spring入门,我们已经可以使用 Spring 框架实现对自定义的 Java 对象管理,由 Spring 框架加载对象,实例化对象,放入容器。其实这就是 Spirng 的核心功能之 IoC,那么什么是 IoC 呢?什么又是容器呢?

什么是 IoC?

来自百度百科的解释 —— 控制反转(IoC):
(Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI),还有一种方式叫 “依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

通俗解释

如何理解好 IoC 呢?上一个小节中,我们使用简单的语言对它做了一个描述 —— IoC 是一种设计模式。将实例化对象的控制权,由手动的 new 变成了 Spring 框架通过反射机制实例化。

那我们来深入分析一下为什么使用 IoC 做控制反转,它到底能帮助我们做什么。
我们假设一个场景:

我们在学习 Web 阶段的过程中,一定实现过数据的查询功能,那么这里我就举一个实例:
我们有这样几个类:

  • UserServlet
  • UserService 接口
  • UserServiceImpl 接口的实现类
  • UserDao

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
UserServlet 作为控制器 接收浏览器的请求
*/
public class UserServlet extends HttpServletRequest {
//用户的业务类 提供逻辑处理 用户相关的方法实现
private UserService userService;

public void service(HttpServletRequest request,HttpServletResponse response){
//手动实例化UserService接口的实现类
userService = new UserServiceImpl();
List<User> list = userService.findAll();
//省略结果的跳转代码

}
}
/*
用户的业务接口UserService
*/
public interface UserService{
public List<User> findAll();
}
/*
UserServiceImpl 作为用户的业务实现类 实现类UserService的接口
*/
public class UserServiceImpl implements UserService{
//用户的Dao
private UserDao userDao;

public List<User> findAll(){
//手动实例化Dao
userDao = new UserDao();
return userDao.findAll();
}
}

问题分析:

上面的代码有什么问题吗? 按照我们学习过的知识… 答案是没有。因为 Dao 只要数据源编写代码正确, 完全可以实现数据的增删改查 ,对吗?

但是分析分析它我们发现:

  1. 代码耦合性太强 不利于程序的测试:
    因为 userServlet 依赖于 userService ,而 userService 依赖于 userDao , 那么只要是被依赖的对象,一定要实例化才行。所以我们采取在程序中硬编码,使用 new 关键字对对象做实例化。 不利于测试,因为你不能确保所有使用的依赖对象都被成功地初始化了。有的朋友很奇怪,对象实例化有什么问题吗? 如果构造参数不满足要求,或者你的构造进行了逻辑处理,那么就有可能实例化失败;
  2. 代码也不利于扩展:
    假设一下,我们花了九牛二虎之气,整理好了所有的类使用的依赖,确保不会产生问题,那么一旦后续我们的方法进行扩充,改造了构造函数,或者判断逻辑,那么是不是所有手动 new 对象的地方都需要更改? 很明显这就不是一个优雅的设计。

解决方式:

Spring 的 IoC 完美的解决了这一点, 对象的实例化由 Spring 框架加载实现,放到 Spring 的容器中管理,避免了我们手动的 new 对象,有需要用到对象实例依赖,直接向 Spring 容器要即可,而一旦涉及到对象的实例修改,那么 只需更改 Spring 加载实例化对象的地方,程序代码无需改动,降低了耦合,提升了扩展性。

容器的使用

刚刚我们解释了 IoC 的作用,是对象的实例化由主动的创建变成了 Spring 的创建,并放入容器管理,那么这个容器是什么?
概念理解:
日常生活中有很多的容器,例如:水桶、茶杯、酒瓶,那么他们都有一个特点,就是装东西。而 Spring 的容器,就是装对象的实例的。

IoC 容器的体系结构

Spring 的容器有两个:

  1. BeanFactory
  2. ApplicationContext

他们两个都是接口,那么有什么区别呢?见图如下:
图片描述

BeanFactory 才是 Spring 容器中的顶层接口。 ApplicationContext 是它的子接口。
简而言之,BeanFactory 提供了配置框架和基本功能,并在 ApplicationContext 中增加了更多针对企业的功能。
BeanFactoryApplicationContext 的区别: 创建对象的时间点不一样。
ApplicationContext:只要一读取配置文件,默认情况下就会创建对象。
BeanFactory:什么时候使用,什么时候创建对象。

IoC 容器实例化的方式

上面已经知道 Spring 的容器是通过一个接口 org.springframework.context.ApplicationContext 表示,并负责实例化,配置和组装 Bean 对象。容器通过读取 xml 文件中的配置信息来获取关于实例化对象,配置属性等命令。
ApplicationContext 只是一个接口,我们通常创建 ClassPathXmlApplicationContext 的实例或者 FileSystemXmlApplicationContext 的实例。前者是从类路径中获取上下文定义文件,后者是从文件系统或 URL 中获取上下文定义文件 。例如:
图片描述

代码解释:
15 行注释掉的代码是通过加载类路径下的配置文件,一般来说 Java 工程放在 src 目录下。我们使用的是 Maven 工程放在 resources 目录下。

18 行代码是通过加载本地 D 盘目录下的文件来初始化容器, 实例化 bean 对象。

结论
通过上面的两种方式测试,发现都可以成功初始化容器, 获取测试的 bean 对象实例。
也证明了容器的初始化可以创建 ClassPathXmlApplicationContext 也可以创建 FileSystemXmlApplicationContext 的实例。

IoC 容器的使用实例

我们知道了加载配置文件初始化容器的方式,现在了解下容器的使用。其实对于我们而言,已经不陌生了,在Spring入门中也已经成功的从容器中获取了对象实例。
这里我们就回顾一下:

1.容器的初始化必须先配置 xml 文件,代码回顾如下:

1
<bean id="user"  class="com.ahao.entity.User" ></beans>

2.加载配置文件

1
2
ApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

3.调用方法

1
context.getBean("user")

小结

本小节对 IoC 概念做了一个详解,同时介绍了 IoC 解决的问题,演示了 IoC 的使用实例,对于初学者来说搞清楚概念,理解作用,实践出结果,就是出色的完成了任务。

Spring IoC (控制反转)之 xml 配置

前言

本小节目的在于带领大家熟练 xml 文件配置, 应用 xml 文件配置 IoC。

Spring入门中我们通过一个入门工程简单地体验了一把 Spring 的使用,梳理了一下 Spring 的工作流程。

大家有了一个初步认知,Spring 框架的工作脱离不了核心配置文件 applicationContext.xml

在配置文件中我们目前只用到了一个 bean 标签,它的作用是用于描述 Java 的类,让框架启动加载配置文件实例化的。

疑问导出

那么我们知道描述一个类有几个要素,类名、属性、构造函数 set 和 get 方法对吧?而 bean 标签如何描述一个详细的类呢?

带着疑问… 开始本节内容。

bean 标签中的属性介绍

核心配置文件回顾

1
<bean id="user" class="com.ahao.entity.User" ></bean>  

在上面的代码中可以看到,在 bean 标签中有两个属性,一个是 id 一个是 class。那么在 bean 标签中都有哪些属性呢?

属性列表

学号 姓名
id 定义的唯一标识
name 同 id 的意义一致
class
factory-bean 工厂对象
factory-method 工厂方法
init-method 初始化执行的方法
destroy-method 销毁执行的方法
scope 对象的作用域
lazy-init 懒加载
autowire 依赖注入
depends-on 依赖于某个实例

疑问导出

上述属性是配置 bean 标签中可以选择的属性,当然一般来讲,我们无需配置所有,可以根据自己的需求配置需要的属性信息,那么如何选择这些属性呢?

属性详细解释

id 和 name 标签的使用

我们目前已经知道所有被实例化后的对象都存在于 Spirng 的容器中,那么从容器中获取这些对象需要一个属性 id 对吧?那么 name 和 id 有什么关系呢?

查看官方文档得知 Spring 的容器会给初始化的每个 bean 都定义一个或多个标识符。这些标识符在容器内必须是唯一的。一个 bean 通常只有一个标识符。而 name 和 id 都可以起到标识符的作用。

所以在 XML 配置文件,我们一般使用 id 或者 name 属性,定义 bean 的唯一标识,这样我们才能通过定义好的唯一标识,从 Spring 的容器中获取他们。

代码实例:

xml 的配置文件如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="user" name="user2" class="com.ahao.entity.User" ></bean>

</beans>

测试代码如下:

1
2
3
4
5
6
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
System.out.println(context.getBean("user"));
System.out.println(context.getBean("user2"));
}

结果如图所示:

image-20211224204058082

结论证明

我们通过 bean 标签中的 id 属性 user, 或者使用 bean 标签中的 name 属性 user2, 都可以得到 Spring 容器中的 user 对象的示例,而且打印的地址是同一个。我们之前说过一句,默认在容器中的实例都是单例的,在这里也得到了证明。

class 属性

bean 标签的定义实质上是创建一个或多个对象的方法。当 xml 文件被解析加载的时候,使用该 bean 定义封装的配置数据来创建(或获取)实际对象,而创建获取的对象是谁呢?就是通过 class 属性中定义的类的全路径来指定 。

一般来讲 class 中的类实例化有两种方式

一种是反射 ,相当于我们使用的 new 关键字。这种也是我们常用的方式。当然不要忘记提供无参数的构造方法(类中默认有无参构造,但是如果自定义了有参构造,默认的无参不会提供)

一种是工厂模式 ,需要借助于 factory-bean 和 factory-method 两个属性,这种方式不常用,我们可以了解下。

factorybean 和 factorymethod 属性

这两个属性主要用于工厂模式实例化 bean 的时候使用,不是很常见。工厂模式有两种,这里分别做个实例,帮助大家理解。

静态工厂模式实例:

1
2
<!--applicationContext的配置bean节点-->
<bean id="user" class="com.ahao.entity.User" factory-method="createUserInstance"/>

创建 bean 示例的 Java 工厂类:

1
2
3
4
5
6
7
8
9
10
public class User {

private static User user = new User();

private User() {}

public static User createInstance() {
return user;
}
}

解释:在定义使用静态工厂方法创建的 bean 时,class 属性指定的是被创建的类,包含静态的方法,并使用 factory-method 属性来指定工厂方法本身名称。

普通工厂模式

1
2
3
4
<!--spring实例化工厂对象 用于创建java实例 -->
<bean id="beanFactory" class="com.ahao.factory.BeanFactory"></bean>
<!-- 被工厂创建的对象实例 -->
<bean id="user1" factory-bean="beanFactory" factory-method="createUser1"/>

工厂类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BeanFactory {

private static User1 user1 = new User1();

private static User2 user2 = new User2();

public User1 createUser1() {
return user1;
}

public User2 createUser2() {
return user2;
}
}

解释:先实例化先创建各个对象示例的工厂对象到容器中,自身的 bean 标签将 class 属性保留为空,并在 factory-bean 属性中指定当前容器中的工厂 Bean 名称,再使用 factory-method 属性设置创建示例的方法名称。

init-method 和 destroy-method 属性的使用

这两个属性比较好理解 init-method 就是 bean 被初始化后执行的方法,destory-method 就是 bean 被销毁执行的代码。

我们来个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class User {

public User(){
System.out.println("我被spring实例化了");
}

public void initMethod(){
System.out.println("user类实例化时候执行的代码");
}
public void destoryMethod(){
System.out.println("user类实例被销毁时候执行的代码");
}
}

配置文件

1
<bean id="user" name="user2" class="com.ahao.entity.User" init-method="initMethod" destroy-method="destoryMethod" ></bean>

测试代码:

1
2
3
4
5
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

}

加载 Spring 的配置文件控制台打印如下:

image-20211224214211633

有个小疑问:销毁语句没打印呢?那是因为并没有调用容器的销毁方法。

改造测试代码如下:

1
2
3
4
5
public static void main(String[] args) {
AbstractApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
context.close();
}

解释:ApplicationContext 没有 close 方法使用它的子类

运行结果:

图片描述

其余属性作用

scope :指定示例的作用范围,后续章节详细讲解;

lazy-init :表示是否为懒加载;

autowire :指定属性注入方式,后续章节详解;

depends-on: 表示是否有依赖的 bean 对象,后续依赖注入章节详细解释。

构造函数的使用

刚刚我们详细解释了 bean 标签内部的属性,经过几个小实例以后不禁也有个问题:

如果我们定义的类中有一些初始化的参数,并且定义好了有参数的构造,通过 xml 配置文件如何体现呢?

实现起来非常简单,跟我来进行一个小实例:

改造 User 类

这是一个普通的 Java 类对象,包含两个属性及其 get 和 set 方法,并且提供了空参构造和有参构造,为了测试方便再覆写一个 toString 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class User {

private Integer id;
private String name;

public User() {
}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
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;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

xml 配置文件方式

1
2
3
4
<bean id="user"  class="com.ahao.entity.User"  >
<constructor-arg name="id" value="1"></constructor-arg>
<constructor-arg name="name" value="zs"></constructor-arg>
</bean>

测试结果

image-20211224214408528

其实对于有参构造实例化对象而言,使用一个标签 constructor-arg 即可,表示构造的参数,如果有多个,可以继续添加,这里不多做演示。

疑问导出

可能有同学会想,那么如果以后我们的属性需要动态更改呢?或者我们的属性不是基本类型而是另外的对象呢? 后续在依赖注入多种属性的小节给大家讲解 。

小结

本章节带着大家详细解释了 bean 标签的使用,那么通过本章节我们收获了哪些呢?

  1. 容器内部命名唯一标识可以通过 id 也可以通过 name;
  2. 实例化对象有两种方式 反射模式和工厂模式;
  3. 如果是反射模式,那么必须配置 class 属性,因为需要用 class 属性中类的全路径来实例化 bean 对象;
  4. 如果需要在类实例化初始化参数,可以使用 init 方法也可以使用有参构造。

Spring 框架模拟实现

前言

通过前面的学习,大家对于 Spring 已经有了初步的认知,我们通过案例练习,或者源码追踪,可以粗略的看到 Spring 框架初始化 bean 对象的过程,那么这个章节,我们模拟 Spring 框架的思路,来写一个类似 Spring 加载对象的案例,加深大家的印象。

案例实现思路

步骤介绍

思路分析:

我们通过写过的案例可以知道:

  1. Spring 框架的容器 是一个接口 ApplicationContext 和接口的实现类 ClassPathXmlApplicationContext 来初始化的;
  2. 在初始化容器对象的时候需要传递 xml 配置文件的位置;
  3. xml 的配置文件中主要是通过 bean 标签可以对 Java 的类进行描述:类的路径 类的标识 类的构造参数等等;
  4. 容器初始化以后需要解析 xml 配置文件的各个 bean 标签;
  5. 实例化的对象如果有参数或者构造方法,那么也需要给参数赋值;

开发准备

为了方便理解测试 ,我们来自定义容器的接口和实现类。

名称改为 SpringContextXmlSpringContext 区别于框架的接口和实现类。

接口定义方法 getBean 用于获取容器内的示例,实现类定义有参构造用于接受初始化时候的配置文件路径。

接口代码如下

1
2
3
public interface SpringContext {
public Object getBean(String beanName);
}

实现类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class XmlSpringContext  implements SpringContext  {

Map<String,Object> map = new HashMap<String,Object>();

public XmlSpringContext (String filename){

}

public Object getBean(String beanName){
return map.get(beanName);
}

}

代码解释

  1. map 用于存储实例化的 bean 对象 ;
  2. 有参构造方法逻辑暂时为空,下面会做实现,加载文件实例化对象在方法内部;
  3. getBean 的方法用于通过 key 获取 map 中存储的实例。

为了测试对象的实例化,我们自定义 UserServiceUserServiceImpl 作为测试的接口对象和实现类。

接口代码如下

1
2
3
4
5
public interface UserService {

public void deleteById(Integer id);

}

接口的实现类代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserServiceImpl implements UserService {
//持久层的dao属性
private UserDao userDao;

public UserDao getUserDao() {
return userDao;
}

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
//实现接口的方法
public void deleteById(Integer id) {

System.out.println("删除的方法执行");
}

}

代码解释:dao 的属性其实是为了模拟属性赋值,后面依赖注入章节会详细讲解。

自定义一个 xml 文件 作为模拟框架的配置文件

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean name="userDao" class="com.ahao.dao.UserDaoImpl"></bean>
<bean name="userService" class="com.ahao.service.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
</beans>

代码解释:userDao 的 bean 需要实例化 是因为 service 用到了它的引用,所以这里多个属性 property。

编写测试类加载文件测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestSpring {

@Test
public void test() {
//初始化容器(读取配置文件 构建工厂)
SpringContext context =
new XmlSpringContext("applicationContext.xml");
UserServiceImpl userService = (UserServiceImpl) context.getBean("userService");
userService.deleteById(1);
System.out.println(userService.getUserDao());
}

}

代码解释:这里的目的只是测试能否获取对象调用方法,如果控制台打印证明案例成功

容器对象的实现类构造函数具体代码

思路分析

  1. 读取初始化时候传递的文件路径;

  2. 通过 SAXReader 解析 xml 文件的节点得到 beans 节点下对应多个 bean 节点集合;

  3. 每一个 bean 表示一个对象,都需要被初始化,所以需要循环遍历集合;

  4. 在循环遍历的过程中获取 id 属性和 class 属性,id 属性作为存入 map 的 key,class 属性用于反射实例化对象,并存储 map 的 value;

  5. 继续解析子节点,如果有参数,反射获取 method 执行参数赋值。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public XmlSpringContext(String filename){

// xml文件的解析器
SAXReader sr = new SAXReader();
try {
//构建一个直接通向我们配置文件路径 的输入流
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(filename);
//文档模型对象
Document doc = sr.read(inputStream);
//获取根标签
Element root = doc.getRootElement();
//获取当前根标签的子标签
List<Element> beans = root.elements("bean");
for(Element bean:beans){
String key = bean.attributeValue("name");
String value = bean.attributeValue("class");
Class<?> myclass = Class.forName(value);
//当前对象
Object obj = myclass.newInstance();
map.put(key, obj);
List<Element> elements = bean.elements("property");
if(elements.size()>0){
for(Element pro: elements){
String av = pro.attributeValue("name");//dao--->setDao
//方法名
String methodName="set"+(av.charAt(0)+"").toUpperCase()+av.substring(1,av.length());
//方法参数
String refvalue = pro.attributeValue("ref");
Object refobj = map.get(refvalue);
//根据方法名称获取方法对象Method
Method method = myclass.getMethod(methodName,refobj.getClass().getInterfaces()[0]);
method.invoke(obj, refobj);
}
}
}

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

测试结果

图片描述

小结

本章节带着大家模拟一下 Spirng 加载文件的过程和实例化对象的过程,当然这个过程只是模拟 Spring 的框架的思路,而并不是真正的 Spring 框架源码,实际源码远比这个要复杂的多,

那么通过本章节我们收获哪些知识呢?

  1. Spring 容器类的使用
  2. xml 配置文件的作用
  3. 反射技术的应用

Spring IoC(控制反转)之注解配置

前言

上两节,我们学习了 Spring IoC 的 xml 配置实现,整理了 xml 方式的实现步骤,并且模拟了 Spring 的容器如何加载解析 xml 配置文件,那么我们发现一点现象:

对于 Spring 的 bean 管理而言,如果全部通过 xml 文件实现的话,配置文件的内容未免过于臃肿。因为对于一个类的实例化,就需要一个 bean 标签。

这样的话,一个大型工程下来,有那么几百个,几千个类,Spring 的 xml 文件维护起来,成本实在太高。

疑问导出

Spring 能否有更方便的方式实现 IoC 呢?Spring 提出了两种 IoC 实现方式,一种是基于配置文件,一种是基于注解形式。

本节,我们学习下 Spring IoC 的注解形式是如何实现的。

案例实现

步骤介绍

回顾 Spring IoC 的 xml 实现步骤

  1. 使用 new 关键字对 ClassPathXmlApplicationContext 做初始化;

  2. 在初始化容器对象的构造传入 xml 配置文件的位置 ;

  3. 在配置文件中通过 bean 标签可以对类进行描述:类的路径、类的标识、类的构造参数等等。

注解实现 IoC 的思路分析:

1.Spring 容器一样需要初始化;

  1. 一样需要传入 xml 配置文件 —– 需要描述清楚 需要被实例化的类都有哪些;

  2. xml 文件中 不需要使用 bean 标签描述被实例化的类 —— 使用注解实现 IoC 管理目的就是为了简化 bean 标签的配置。

疑问导出:

如果是 xml 文件方式实现 IoC ,加载 xml 文件的 bean 标签就已经知道,需要被实例化的对象,那么如果不使用 bean 标签描述,Spring 框架如何得知哪些类需要被容器管理呢?

核心思想:

开发人员无需使用 XML 来描述 bean ,而是将配置移入 Java 的类本身,通过 Spring 支持的组件扫描来实现。

看官稍等… 马上开始我们的案例实现。

工程实现

创建工程

为了区分 xml 工程,坐标名称换成 spring_an ,其实无所谓,大家自行创建即可。

image-20211224220118299

导入依赖

依赖的坐标跟 xml 的工程坐标一致即可,无需导入多余的依赖。

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
</dependencies>

项目代码

为了测试,在工程内部创建 UserDao 的接口和 UserDao 的实现类 UserDaoImpl

UserDao 代码如下:

1
2
3
4
public interface UserDao {

public void saveUser();
}

UserDaoImpl 的实现类代码如下:

1
2
3
4
5
6
7
@Repository
public class UserDaoImpl implements UserDao {

public void saveUser() {
System.out.println("执行dao的保存方法");
}
}

注意事项: 由于我们是基于注解的方式实现对 bean 的管理,所以在实现类上面需要添加一个注解 @Repository,此注解的作用是为了 Spring 的容器启动后,需要要自动检测这些被注解的类并注册相应的 bean 实例到容器中。

Spring 的核心配置文件

1
2
3
4
5
6
7
8
9
10
11
12
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.ahao.dao"></context:component-scan>

</beans>

上面是本案例的配置文件,那么可以看出跟 xml 的配置文件有很大的区别:

配置节点context-component-scan 标签,这是 Spring 框架自定义的 xml 标签,通过 base-package 的属性,指明需要被自动扫描实例化的类所在位置。

如上图所示,我们在 com.ahao.dao 下的类是需要扫描自动注入容器的。

小细节:不是在 com.ahao.dao 下的所有类都会自动注入到容器,而是要搭配注解:比如我们的 @Repository 当然还有其余的注解,我们后面章节会详细讲解。

测试类测试结果

image-20211224221233902

代码解释

测试类其实跟 xml 的方式一模一样,我们本次测试的目的一样也是通过 Spring 容器管理注册的 bean 对象,只不过对象的实例化方式换成了注解,那么我们看到成功输出在控制台的测试语句,说明案例搭建完成。

小结

本节带着大家使用注解的方式,实现了 Spring 对于 bean 的管理。

那么回顾下注解开发的步骤和注意点:

  1. Spring 容器初始化一样需要 xml 文件,目前是 xml 文件搭配注解管理 bean 并不是纯注解开发;
  2. Spring 的 xml 配置文件中使用 context:component-scan 标签指定注册 bean 的类所在目录位置;
  3. 自定义编写的 Java 类,如果需要被自动扫描注入容器,必须搭配注解。

Spring IoC(控制反转)之常用注解

前言

上一节,我们通过注解的方式,实现了 Spring 对于 bean 的管理,那么如何实现的,我们回顾一下

两个重要点:

  1. 注解实例化的类上,需要使用一个注解 @Repository
  2. Spring 的配置文件中,需要使用组件扫描 <context:component-scan>

疑问导出

组件扫描的作用我们清楚,是为了扫描路径之下带有注解的类,但是为什么类上面的注解是 @Repository 呢?或者说,是否还有其余的注解可以实现呢?

本节,我们一起来学习下 Spring IoC 的常用注解。

注解的详解

在我们详细讲解注解之前,首先明确一点:

注解配置和 xml 配置实现的功能都是一样的,只不过实现的方式不同,那么也就是说,xml 文件可以实现的,通过注解都可以完全办得到。比如实例化对象,设置属性,设置作用范围,生命周期的方法执行等等…

注解分类介绍

按功能划分

  1. 创建对象: 对应的就是在 xml 文件中配置的一个 bean 标签,可以定义 id、name、class 等属性;
  2. 注入数据: 对应的就是在 bean 标签下,使用 property 标签给类中的依赖属性赋值;
  3. 作用范围: 对应的就是设置 bean 标签的 scope 属性,不设置默认也是单例;
  4. 生命周期: 对应的就是设置 bean 标签的 init-method 和 destroy-method 方法。

创建对象的注解介绍

从 Spring 的官网得知一段话:

@Repository 注释是针对满足的存储库(也被称为数据访问对象或 DAO)的作用,或者固定型的任何类的标记。

也就是说,我们上一节中使用的注解,一般用于 dao 层使用。那么,我们都知道,JAVAEE 体系结构,一般开发分为三个层级:

  1. 表现层: 主要作用为处理数据生成静态的页面响应给浏览器展示 ;
  2. 业务层: 主要作用为业务逻辑代码编写,数据的获取,数据的封装返回等等操作都在这里;
  3. 持久层: 主要作用为跟数据库打交道,对于数据的持久化操作等。

那么,如果是创建的表现层或者业务层代码,应该使用什么注解呢?

好了,看一下创建对象注解的划分:

  1. @Component :一般用于通用组件的类上使用的注解;
  2. @Service : 一般用于业务层类上使用的注解;
  3. @Controller : 一般用于控制层类上使用的注解;
  4. @Repository :一般用于持久层类上使用的注解。

官网解释

Spring 提供进一步典型化注解:@Component@Service,和 @Dao
@Component 是任何 Spring 托管组件的通用构造型。
@Repository@Service@Controller@Component 针对更特定用例的专业化(分别在持久性,服务和表示层)。

通俗解释:

@Component 注解是 Spring 框架中通用的一个注解,用于组件扫描实例化对象使用, 那么其余的三个注解 @Controller@Service@Repository 都是 @Component 注解的衍生注解,作用跟 @Componet 注解的作用一致。
那么意义在于, 三个注解,对应的是三个开发层级 ,一般来讲我们将 @Controller 作为表现层的使用,@Service 作为业务层的注解,@Repository 作为持久层使用的注解。我们下面通过案例演示一下。

创建对象的注解

实例说明

四种注解的测试,本节重点讲解创建对象使用的注解,而作用范围 scope 和生命周期的两个注解,我们放在后续对应的小节进行讲解测试。

置于注入数据的注解,是比较重要的一个内容, 我们放在依赖注入这节详细讲解。

创建工程省略

我们继续使用上一节的注解工程实例即可,那么为了演示三个注解,我们分别创建三个层级对应的代码:

  • 表现层的 UserController
  • 业务层的 UserService
  • 实现类 UserServiceImpl

持久层 dao 代码已经创建过了,这里不多解释。创建好的所有代码如下:

UserController 代码:

1
2
3
4
5
6
7
@Controller
public class UserController {

public void saveUser(){
System.out.println("这是controller的执行保存..");
}
}

UserService 和实现类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface UserService {

public void saveUser();
}

@Service
public class UserServiceImpl implements UserService {

public void saveUser() {

System.out.println("执行service中的保存逻辑");
}
}

项目结构如下:

image-20211224222021654

上面是本案例的工程以及代码结构:

类虽然看起来很多,实际没有业务逻辑代码,只不过在各个层级使用了三个注解来注入到容器,目的是测试当 Spring 的配置文件加载扫描后,是否可以从容器中获取三种注解(@Controller @Service @Repository)注入的 bean 对象。

Tips: Spring 的配置文件 context:component-scan 标签的扫描层级 需要包含三个包路径,例如我的工程实例代码如下:

1
<context:component-scan base-package="com.ahao"></context:component-scan>

测试类与测试结果

image-20211224223258347

结论:

可以三个注解都可以将对象注入到 Spring 的容器,那么以后开发时候按照规范或者习惯,分层开发,使用对应的注解。但它并不是必须这么做,你使用任意一种都可以,只不过,代码的可读性会差。

所以,我们一般表现层使用 @controller ,业务层使用 @service, 持久层使用 @Repository

至于 @Component 如果有其余的类,不属于三个层级,可以采用 @Component 作为通用组件扫描注入容器。

注解注入规则

刚刚通过三个注解都可以完成了 bean 的实例化注入,通过测试代码也获取到了容器中的三个对象实例,那么这里不知道大家是否发现一个问题:

我们知道,Spring 这个容器本质是个 map 集合来存储实例化后的对象。既然是个 map 集合,就应该对应的有 key 和 value。

我们都知道 value 肯定是实例化后的 bean ,那么 key 是什么呢?

注入规则

  1. 四种注解都支持 value 的属性作为自定义的 bean id ;
  2. 如果 value 属性没有指定,那么默认以类的简单名称(类名首字母小写)作为 bean 对象的 id。

所以我们可以看到:

当我们只使用注解没有自定义 id 的时候可以通过,每个类的首字母小写来获取对象实例,那么如果有了自定义的 id,上述代码是否继续可用呢?

自定义 id 获取实例

改造类上面的注解,设置自定的 id,更改的注解如下:

1
2
3
@Controll("uc")
@Service("us")
@Repository("ud")

测试结果

image-20211224225011452

测试结果

为了区分测试结果,我在测试代码中,只修改了 controller 的获取方式,将 id 改成了 uc 。service 和 dao 并没有修改。

从控制台打印可以看到,只有 controller 对象可以成功获取,service 和 dao 都失败了,因为我们已经使用了自定义的 id,所以容器中没有默认的以类名作为 id 的 bean 对象实例。

小结

本章节重点讲解注解的使用:

  1. Spring 支持的注解有四种分类;
  2. Spring 创建对象的注解四种分类;
  3. Spring 创建对象注入容器的规则。