一、Spring启示录
阅读以下代码:
package com.powernode.oa.controller;
import com.powernode.oa.service.UserService;
import com.powernode.oa.service.impl.UserServiceImpl;
public class UserController {
private UserService userService = new UserServiceImpl();
public void login(){
String username = "admin";
String password = "123456";
boolean success = userService.login(username, password);
if (success) {
// 登录成功
} else {
// 登录失败
}
}
}
package com.powernode.oa.service.impl;
import com.powernode.oa.bean.User;
import com.powernode.oa.dao.UserDao;
import com.powernode.oa.dao.impl.UserDaoImplForMySQL;
import com.powernode.oa.service.UserService;
public class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImplForMySQL();
public boolean login(String username, String password) {
User user = userDao.selectByUsernameAndPassword(username, password);
if (user != null) {
return true;
}
return false;
}
}
package com.powernode.oa.dao.impl;
import com.powernode.oa.bean.User;
import com.powernode.oa.dao.UserDao;
public class UserDaoImplForMySQL implements UserDao {
public User selectByUsernameAndPassword(String username, String password) {
// 连接MySQL数据库,根据用户名和密码查询用户信息
return null;
}
}
可以看出,UserDaoImplForMySQL中主要是连接MySQL数据库进行操作。如果更换到Oracle数据库上,则需要再提供一个UserDaoImplForOracle,如下:
package com.powernode.oa.dao.impl;
import com.powernode.oa.bean.User;
import com.powernode.oa.dao.UserDao;
public class UserDaoImplForOracle implements UserDao {
public User selectByUsernameAndPassword(String username, String password) {
// 连接Oracle数据库,根据用户名和密码查询用户信息
return null;
}
}
很明显,以上的操作正在进行功能的扩展,添加了一个新的类UserDaoImplForOracle来应付数据库的变化,这里的变化会引起连锁反应吗?当然会,如果想要切换到Oracle数据库上,UserServiceImpl类代码就需要修改,如下:
package com.powernode.oa.service.impl;
import com.powernode.oa.bean.User;
import com.powernode.oa.dao.UserDao;
import com.powernode.oa.dao.impl.UserDaoImplForOracle;
import com.powernode.oa.service.UserService;
public class UserServiceImpl implements UserService {
//private UserDao userDao = new UserDaoImplForMySQL();
private UserDao userDao = new UserDaoImplForOracle();
public boolean login(String username, String password) {
User user = userDao.selectByUsernameAndPassword(username, password);
if (user != null) {
return true;
}
return false;
}
}
1.1 OCP开闭原则
这样一来就违背了开闭原则OCP。开闭原则是这样说的:在软件开发过程中应当对扩展开放,对修改关闭。也就是说,如果在进行功能扩展的时候,添加额外的类是没问题的,但因为功能扩展而修改之前运行正常的程序,这是忌讳的,不被允许的。因为一旦修改之前运行正常的程序,就会导致项目整体要进行全方位的重新测试。这是相当麻烦的过程。导致以上问题的主要原因是:代码和代码之间的耦合度太高。如下图所示:
可以很明显的看出,上层是依赖下层的。UserController依赖UserServiceImpl,而UserServiceImpl依赖UserDaoImplForMySQL,这样就会导致下面只要改动,上面必然会受牵连(跟着也会改),所谓牵一发而动全身。这样也就同时违背了另一个开发原则:依赖倒置原则。
1.2 依赖倒置原则DIP
依赖倒置原则(Dependence Inversion Principle),简称DIP,主要倡导面向抽象编程,面向接口编程,不要面向具体编程,让上层不再依赖下层,下面改动了,上面的代码不会受到牵连。这样可以大大降低程序的耦合度,耦合度低了,扩展力就强了,同时代码复用性也会增强。(软件七大开发原则都是在为解耦合服务)
你可能会说,上面的代码已经面向接口编程了呀:
确实已经面向接口编程了,但对象的创建是:new UserDaoImplForOracle()显然并没有完全面向接口编程,还是使用到了具体的接口实现类。什么叫做完全面向接口编程?什么叫做完全符合依赖倒置原则呢?请看以下代码:
如果代码是这样编写的,才算是完全面向接口编程,才符合依赖倒置原则。那你可能会问,这样userDao是null,在执行的时候就会出现空指针异常呀。你说的有道理,确实是这样的,所以我们要解决这个问题。解决空指针异常的问题,其实就是解决两个核心的问题:
- 第一个问题:谁来负责对象的创建。【也就是说谁来:new UserDaoImplForOracle()/new UserDaoImplForMySQL()】
- 第二个问题:谁来负责把创建的对象赋到这个属性上。【也就是说谁来把上面创建的对象赋给userDao属性】
如果我们把以上两个核心问题解决了,就可以做到既符合OCP开闭原则,又符合依赖倒置原则。
很荣幸的通知你:Spring框架可以做到。
在Spring框架中,它可以帮助我们new对象,并且它还可以将new出来的对象赋到属性上。换句话说,Spring框架可以帮助我们创建对象,并且可以帮助我们维护对象和对象之间的关系。比如:
Spring可以new出来UserDaoImplForMySQL对象,也可以new出来UserDaoImplForOracle对象,并且还可以让new出来的dao对象和service对象产生关系(产生关系其实本质上就是给属性赋值)。
很显然,这种方式是将对象的创建权/管理权交出去了,不再使用硬编码的方式了。同时也把对象关系的管理权交出去了,也不再使用硬编码的方式了。像这种把对象的创建权交出去,把对象关系的管理权交出去,被称为控制反转。
1.3 控制反转IoC
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计思想,可以用来降低代码之间的耦合度,符合依赖倒置原则。
控制反转的核心是:将对象的创建权交出去,将对象和对象之间关系的管理权交出去,由第三方容器来负责创建与维护。
控制反转常见的实现方式:依赖注入(Dependency Injection,简称DI)
通常,依赖注入的实现又包括两种方式:
- set方法注入
- 构造方法注入
而Spring框架就是一个实现了IoC思想的框架。
IoC可以认为是一种全新的设计模式,但是理论和时间成熟相对较晚,并没有包含在GoF中。(GoF指的是23种设计模式)
对象和对象之间的关系的维护权:即UserServiceImpl中UserDao userDao的具体实现是UserDaoImplForMySQL还是UserDaoImplForOracle
public class UserServiceImpl implements UserService {
//private UserDao userDao = new UserDaoImplForMySQL();
// 修改为
//private UserDao userDao = new UserDaoImplForOracle();
private UserDao userDao;
@Override
public void deleteUser() {
userDao.deleteById();
}
}
1.4 课堂笔记
1. OCP开闭原则
* 什么是OCP?
OCP是软件七大开发原则当中最基本的一个原则:开闭原则,即开放封闭原则(Open Closed Principle)
对什么开?对扩展开放。
对什么闭?对修改关闭。
* OCP原则是最核心的,最基本的,其他的六个原则都是为这个原则服务的。
* OCP开闭原则的核心是什么?
只要你在扩展系统功能的时候,没有修改以前写好的代码,那么你就是符合OCP原则的。
反之,如果在扩展系统功能的时候,你修改了之前的代码,那么这个设计是失败的,违背OCP原则。
* 当进行系统功能扩展的时候,如果动了之前稳定的程序,修改了之前的程序,之前所有程序都需要进行重新测试。这是不想看到的,因为非常麻烦。
2. 依赖倒置原则(DIP原则)
* 什么是依赖倒置原则?
面向接口编程,面向抽象编程,不要面向具体编程。
* 依赖倒置原则的目的?
降低程序的耦合度,提高扩展力。
* 什么叫做符合依赖倒置?
上 不依赖 下,就是符合。
* 什么叫做违背依赖倒置?
上 依赖 下,就是违背。
只要“下”一改动,“上”就受到牵连。
3. 当前程序的设计,显然既违背OCP,又违背DIP,怎么办?
可以采用“控制反转”这种编程思想来解决这个问题。
4. 什么是控制反转?
控制反转:IoC(Inversion of Control)
反转是什么呢?
反转的是两件事:
第一件事:我不在程序中采用硬编码的方式来new对象了。(new对象我不管了,new对象的权利交出去了。)
第二件事:我不在程序中采用硬编码的方式来维护对象的关系了。(对象之间关系的维护权,我也不管了,交出去了。)
控制反转:是一种编程思想。或者叫做一种新型的设计模式。由于出现的比较新,没有被纳入GoF23种设计模式范围内。
5. Spring框架
* Spring框架实现了控制反转IoC这种思想
Spring框架可以帮你new对象。
Spring框架可以帮你维护对象和对象之间的关系。
* Spring是一个实现了IoC思想的容器。
* 控制反转的实现方式有多种,其中比较重要的叫做:依赖注入(Dependency Injection,简称DI)。
* 控制反转是思想。依赖注入是这种思想的具体实现。
* 依赖注入DI,又包括常见的两种方式:
第一种:set注入(执行set方法给属性赋值)
第二种:构造方法注入(执行构造方法给属性赋值)
* 依赖注入 中 “依赖”是什么意思? “注入”是什么意思?
依赖:A对象和B对象的关系。
注入:是一种手段,通过这种手段,可以让A对象和B对象产生关系。
依赖注入:对象A和对象B之间的关系,靠注入的手段来维护。而注入包括:set注入和构造注入。
6. 注意术语:
OCP:开闭原则(开发原则)
DIP:依赖倒置原则(开发原则)
IoC:控制反转(一种思想,一种新型的设计模式)
DI:依赖注入(控制反转思想的具体实现方式)
二、Spring概述
2.1 Spring简介
来自百度百科
Spring是一个开源框架,它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。
从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
Spring最初的出现是为了解决EJB臃肿的设计,以及难以测试等问题。
Spring为简化开发而生,让程序员只需关注核心业务的实现,尽可能的不再关注非业务逻辑代码(事务控制,安全日志等)。
2.2 Spring8大模块
注意:Spring5版本之后是8个模块。在Spring5中新增了WebFlux模块。
- Spring Core模块
这是Spring框架最基础的部分,它提供了依赖注入(DependencyInjection)特征来实现容器对Bean的管理。核心容器的主要组件是 BeanFactory,BeanFactory是工厂模式的一个实现,是任何Spring应用的核心。它使用IoC将应用配置和依赖从实际的应用代码中分离出来。
- Spring Context模块
如果说核心模块中的BeanFactory使Spring成为容器的话,那么上下文模块就是Spring成为框架的原因。
这个模块扩展了BeanFactory,增加了对国际化(I18N)消息、事件传播、验证的支持。另外提供了许多企业服务,例如电子邮件、JNDI访问、EJB集成、远程以及时序调度(scheduling)服务。也包括了对模版框架例如Velocity和FreeMarker集成的支持
- Spring AOP模块
Spring在它的AOP模块中提供了对面向切面编程的丰富支持,Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖组件,就可以将声明性事务管理集成到应用程序中,可以自定义拦截器、切点、日志等操作。
- Spring DAO模块
提供了一个JDBC的抽象层和异常层次结构,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析,用于简化JDBC。
- Spring ORM模块
Spring提供了ORM模块。Spring并不试图实现它自己的ORM解决方案,而是为几种流行的ORM框架提供了集成方案,包括Hibernate、JDO和iBATIS SQL映射,这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
- Spring Web MVC模块
Spring为构建Web应用提供了一个功能全面的MVC框架。虽然Spring可以很容易地与其它MVC框架集成,例如Struts,但Spring的MVC框架使用IoC对控制逻辑和业务对象提供了完全的分离。
- Spring WebFlux模块
Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版的后期添加的。它是完全非阻塞的,支持反应式流(Reactive Stream)背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。
- Spring Web模块
Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文,提供了Spring和其它Web框架的集成,比如Struts、WebWork。还提供了一些面向服务支持,例如:实现文件上传的multipart请求。
2.3 Spring特点
- 轻量
- 从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。
- Spring是非侵入式的:Spring应用中的对象不依赖于Spring的特定类。(Spring框架的运行不依赖于任何别的框架)
- 控制反转
- Spring通过一种称作控制反转(IoC)的技术促进了松耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。
- 面向切面
- Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。
- 容器
- Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。
- 框架
- Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。
所有Spring的这些特征使你能够编写更干净、更可管理、并且更易于测试的代码。它们也为Spring中的各种模块提供了基础支持。
2.4 本教程软件版本
- IDEA工具:2022.1.4
- JDK:Java17 (Spring6要求JDK最低版本是Java17)
- Maven:3.8.6
- Spring:6.0.0-M2
- JUnit:4.13.2
三、Spring的入门程序
3.1 Spring的下载
官网地址:https://spring.io/
官网地址(中文):http://spring.p2hp.com/
打开Spring官网后,可以看到Spring Framework,以及通过Spring Framework衍生的其它框架:
我们即将要学习的就是Spring Framework。
怎么下载呢?
- 第一步:进入github
- 第二步:找到下图位置,点击超链接
- 第三步:找到下图位置,点击超链接
- 第四步:按照下图步骤操作
- 第五步:继续在springframework目录下找下图的spring,点开之后你会看到很多不同的版本
- 第六步:选择对应的版本
- 第七步:点击上图的url
点击spring-5.3.9-dist.zip下载spring框架。
将下载的zip包解压:
docs:spring框架的API帮助文档
libs:spring框架的jar文件(用spring框架就是用这些jar包)
schema:spring框架的XML配置文件相关的约束文件
3.2 Spring的jar文件
打开libs目录,会看到很多jar包:
spring-core-5.3.9.jar:字节码(这个是支撑程序运行的jar包)
spring-core-5.3.9-javadoc.jar:代码中的注释
spring-core-5.3.9-sources.jar:源码
我们来看一下spring框架都有哪些jar包:
JAR文件 | 描述 |
---|---|
spring-aop-5.3.9.jar | 这个jar 文件包含在应用中使用Spring 的AOP 特性时所需的类 |
spring-aspects-5.3.9.jar | 提供对AspectJ的支持,以便可以方便的将面向切面的功能集成进IDE中 |
spring-beans-5.3.9.jar | 这个jar 文件是所有应用都要用到的,它包含访问配置文件、创建和管理bean 以及进行Inversion ofControl / Dependency Injection(IoC/DI)操作相关的所有类。如果应用只需基本的IoC/DI 支持,引入spring-core.jar 及spring-beans.jar 文件就可以了。 |
spring-context-5.3.9.jar | 这个jar 文件为Spring 核心提供了大量扩展。可以找到使用Spring ApplicationContext特性时所需的全部类,JDNI 所需的全部类,instrumentation组件以及校验Validation 方面的相关类。 |
spring-context-indexer-5.3.9.jar | 虽然类路径扫描非常快,但是Spring内部存在大量的类,添加此依赖,可以通过在编译时创建候选对象的静态列表来提高大型应用程序的启动性能。 |
spring-context-support-5.3.9.jar | 用来提供Spring上下文的一些扩展模块,例如实现邮件服务、视图解析、缓存、定时任务调度等 |
spring-core-5.3.9.jar | Spring 框架基本的核心工具类。Spring 其它组件要都要使用到这个包里的类,是其它组件的基本核心,当然你也可以在自己的应用系统中使用这些工具类。 |
spring-expression-5.3.9.jar | Spring表达式语言。 |
spring-instrument-5.3.9.jar | Spring3.0对服务器的代理接口。 |
spring-jcl-5.3.9.jar | Spring的日志模块。JCL,全称为"Jakarta Commons Logging",也可称为"Apache Commons Logging"。 |
spring-jdbc-5.3.9.jar | Spring对JDBC的支持。 |
spring-jms-5.3.9.jar | 这个jar包提供了对JMS 1.0.2/1.1的支持类。JMS是Java消息服务。属于JavaEE规范之一。 |
spring-messaging-5.3.9.jar | 为集成messaging api和消息协议提供支持 |
spring-orm-5.3.9.jar | Spring集成ORM框架的支持,比如集成hibernate,mybatis等。 |
spring-oxm-5.3.9.jar | 为主流O/X Mapping组件提供了统一层抽象和封装,OXM是Object Xml Mapping。对象和XML之间的相互转换。 |
spring-r2dbc-5.3.9.jar | Reactive Relational Database Connectivity (关系型数据库的响应式连接) 的缩写。这个jar文件是Spring对r2dbc的支持。 |
spring-test-5.3.9.jar | 对Junit等测试框架的简单封装。 |
spring-tx-5.3.9.jar | 为JDBC、Hibernate、JDO、JPA、Beans等提供的一致的声明式和编程式事务管理支持。 |
spring-web-5.3.9.jar | Spring集成MVC框架的支持,比如集成Struts等。 |
spring-webflux-5.3.9.jar | WebFlux是 Spring5 添加的新模块,用于 web 的开发,功能和 SpringMVC 类似的,Webflux 使用当前一种比较流程响应式编程出现的框架。 |
spring-webmvc-5.3.9.jar | SpringMVC框架的类库 |
spring-websocket-5.3.9.jar | Spring集成WebSocket框架时使用 |
注意:
如果你只是想用Spring的IoC功能,仅需要引入:spring-context即可。将这个jar包添加到classpath当中。
如果采用maven只需要引入context的依赖即可。
<!--Spring6的正式版发布之前,这个仓库地址是需要的-->
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖:使用的是6.0.0-M2里程碑版-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
</dependencies>
3.3 第一个Spring程序
前期准备:
- 打开IDEA创建Empty Project:spring6
- 设置JDK版本17,编译器版本17
- 设置IDEA的Maven:关联自己的maven
- 在空的工程spring6中创建第一个模块:spring6-001-first
第一步:添加spring context的依赖,pom.xml配置如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-001-first</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<!--如果,你想使用spring的jdbc,或者说其他的tx,那么还需要再次添加依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
注意:打包方式jar。
当加入spring context的依赖之后,会关联引入其他依赖:
spring aop:面向切面编程
spring beans:IoC核心
spring core:spring的核心工具包
spring jcl:spring的日志包
spring expression:spring表达式
第二步:添加junit依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-001-first</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第三步:定义bean:User
package com.powernode.spring6.bean;
/**
* bean,封装用户信息。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class User {
}
第四步:编写spring的配置文件:beans.xml。该文件放在类的根路径下。
上图是使用IDEA工具自带的spring配置文件的模板进行创建。
配置文件中进行bean的配置。
<?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">
<!--这就是Spring的配置文件-->
<!--IDEA工具为我们提供了这个文件的模板,一定要使用这个模板来创建。-->
<!--这个文件名不一定叫做spring.xml,可以是其它名字。-->
<!--这个文件最好是放在类路径当中,方便后期的移植。-->
<!--放在resources根目录下,就相当于是放到了类的根路径下。-->
<!--配置bean,这样spring才可以帮助我们管理这个对象。-->
<!--
bean标签的两个重要属性:
id:是这个bean的身份证号,不能重复,是唯一的标识。
class:必须填写类的全路径,全限定类名。(带包名的类名)
-->
<bean id="userBean" class="com.powernode.spring6.bean.User"/>
<!--<bean id="userBean" class="com.powernode.spring6.bean.Vip"/>-->
<!--配置其他的bean-->
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDaoImplForMySQL"/>
<!--配置java.util.Date Bean-->
<bean id="nowTime" class="java.util.Date"/>
</beans>
bean的id和class属性:
- id属性:代表对象的唯一标识。可以看做一个人的身份证号。
- class属性:用来指定要创建的java对象的类名,这个类名必须是全限定类名(带包名)。
第五步:编写测试程序
package com.powernode.spring6.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Spring6Test {
@Test
public void testFirst(){
// 第一步:获取Spring容器对象。初始化Spring容器上下文(解析beans.xml文件,创建所有的bean对象)
// ApplicationContext 翻译为:应用上下文。其实就是Spring容器。
// ApplicationContext 是一个接口。
// ApplicationContext 接口下有很多实现类。其中有一个实现类叫做:ClassPathXmlApplicationContext
// ClassPathXmlApplicationContext 专门从类路径当中加载spring配置文件的一个Spring上下文对象。
// 这行代码只要执行:就相当于启动了Spring容器,解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中。
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 第二步:根据bean的id从Spring容器中获取这个对象。
Object userBean = applicationContext.getBean("userBean");
System.out.println(userBean);
}
}
第七步:运行测试程序
3.4 第一个Spring程序详细剖析
<bean id="userBean" class="com.powernode.spring6.bean.User"/>
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
Object userBean = applicationContext.getBean("userBean");
- bean标签的id属性可以重复吗?
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Vip
* @since 1.0
**/
public class Vip {
}
<?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="userBean" class="com.powernode.spring6.bean.User"/>
<bean id="userBean" class="com.powernode.spring6.bean.Vip"/>
</beans>
运行测试程序:
通过测试得出:在spring的配置文件中id是不能重名。
- 底层是怎么创建对象的,是通过反射机制调用无参数构造方法吗?
package com.powernode.spring6.bean;
/**
* bean,封装用户信息。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class User {
// Spring是怎么实例化对象的?
// 默认情况下Spring会通过反射机制,调用类的无参数构造方法来实例化对象。
// 实现原理如下:
// Class clazz = Class.forName("com.powernode.spring6.bean.User");
// Object obj = clazz.newInstance();
public User() {
System.out.println("User的无参数构造方法执行");
}
}
在User类中添加无参数构造方法,如上。
运行测试程序:
通过测试得知:创建对象时确实调用了无参数构造方法。
如果提供一个有参数构造方法,不提供无参数构造方法会怎样呢?
package com.powernode.spring6.bean;
/**
* bean,封装用户信息。
* @author 动力节点
* @version 1.0
* @since 1.0
*/
public class User {
/*public User() {
System.out.println("User的无参数构造方法执行");
}*/
public User(String name){
System.out.println("User的有参数构造方法执行");
}
}
运行测试程序:
通过测试得知:spring是通过调用类的无参数构造方法来创建对象的,所以要想让spring给你创建对象,必须保证无参数构造方法是存在的。
Spring是如何创建对象的呢?原理是什么?
// dom4j解析beans.xml文件,从中获取class的全限定类名
// 通过反射机制调用无参数构造方法创建对象
Class clazz = Class.forName("com.powernode.spring6.bean.User");
Object obj = clazz.newInstance();
- 把创建好的对象存储到一个什么样的数据结构当中了呢?
- spring配置文件的名字必须叫做beans.xml吗?
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
通过以上的java代码可以看出,这个spring配置文件名字是我们负责提供的,显然spring配置文件的名字是随意的。
- 像这样的beans.xml文件可以有多个吗?
再创建一个spring配置文件,起名:spring.xml
<?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="vipBean" class="com.powernode.spring6.bean.Vip"/>
</beans>
package com.powernode.spring6.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Spring6Test {
@Test
public void testFirst(){
// 第一步:获取Spring容器对象。初始化Spring容器上下文(解析beans.xml文件,创建所有的bean对象)
// ApplicationContext 翻译为:应用上下文。其实就是Spring容器。
// ApplicationContext 是一个接口。
// ApplicationContext 接口下有很多实现类。其中有一个实现类叫做:ClassPathXmlApplicationContext
// ClassPathXmlApplicationContext 专门从类路径当中加载spring配置文件的一个Spring上下文对象。
// 这行代码只要执行:就相当于启动了Spring容器,解析spring.xml文件,并且实例化所有的bean对象,放到spring容器当中。
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml", "beans.xml");
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml", "beans.xml", "xml/beans.xml");
// 第二步:根据bean的id从Spring容器中获取这个对象。
Object userBean = applicationContext.getBean("userBean");
Object vipBean = applicationContext.getBean("vipBean");
Object userBean2 = applicationContext.getBean("userBean2");
System.out.println(userBean);
System.out.println(vipBean);
System.out.println(userBean2);
}
}
运行测试程序:
通过测试得知,spring的配置文件可以有多个,在ClassPathXmlApplicationContext构造方法的参数上传递文件路径即可。这是为什么呢?通过源码可以看到:
- 在配置文件中配置的类必须是自定义的吗,可以使用JDK中的类吗,例如:java.util.Date?
<?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="userBean" class="com.powernode.spring6.bean.User"/>
<!--<bean id="userBean" class="com.powernode.spring6.bean.Vip"/>-->
<!--配置java.util.Date Bean-->
<bean id="dateBean" class="java.util.Date"/>
</beans>
package com.powernode.spring6.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Spring6Test {
@Test
public void testFirst(){
// 初始化Spring容器上下文(解析beans.xml文件,创建所有的bean对象)
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml","spring6.xml");
// 根据id获取bean对象
Object userBean = applicationContext.getBean("userBean");
Object vipBean = applicationContext.getBean("vipBean");
Object dateBean = applicationContext.getBean("dateBean");
System.out.println(userBean);
System.out.println(vipBean);
System.out.println(dateBean);
}
}
运行测试程序:
通过测试得知,在spring配置文件中配置的bean可以任意类,只要这个类不是抽象的,并且提供了无参数构造方法。
- getBean()方法调用时,如果指定的id不存在会怎样?
运行测试程序:
通过测试得知,当id不存在的时候,会出现异常。
- getBean()方法返回的类型是Object,如果访问子类的特有属性和方法时,还需要向下转型,有其它办法可以解决这个问题吗?
// 不想强制类型转换,可以使用以下代码(通过第二个参数来指定返回的bean的类型。)
User user = applicationContext.getBean("userBean", User.class);
- ClassPathXmlApplicationContext是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?
<?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="vipBean2" class="com.powernode.spring6.bean.Vip"/>
</beans>
@Test
public void testXmlPath(){
// FileSystemXmlApplicationContext 不是从类路径当中加载资源。
// 这种方式很少用。了解即可。
ApplicationContext applicationContext = new FileSystemXmlApplicationContext("d:/spring6.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}
没有在类路径中的话,需要使用FileSystemXmlApplicationContext类进行加载配置文件。
这种方式较少用。一般都是将配置文件放到类路径当中,这样可移植性更强。
- ApplicationContext的超级父接口BeanFactory。
Spring底层的IoC是怎么实现的?XML解析 + 工厂模式 + 反射机制
@Test
public void testBeanFactory(){
//ApplicationContext接口的超级父接口是:BeanFactory(翻译为Bean工厂,就是能够生产Bean对象的一个工厂对象。)
//BeanFactory是IoC容器的顶级接口。
//Spring的IoC容器底层实际上使用了:工厂模式。
//Spring底层的IoC是怎么实现的?XML解析 + 工厂模式 + 反射机制
//ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
BeanFactory applicationContext = new ClassPathXmlApplicationContext("spring6.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}
BeanFactory是Spring容器的超级接口。ApplicationContext是BeanFactory的子接口。
- 开始初始化bean的时机
@Test
public void testBeginInitBean(){
// 注意:不是在调用getBean()方法的时候创建对象,执行以下代码的时候,就会实例化对象。
new ClassPathXmlApplicationContext("spring6.xml");
}
3.5 Spring6启用Log4j2日志框架
从Spring5之后,Spring框架支持集成的日志框架是Log4j2.如何启用日志框架:
第一步:引入Log4j2的依赖
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
第二步:在类的根路径下提供log4j2.xml配置文件(文件名固定为:log4j2.xml,文件必须放到类根路径下。)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<!--
level指定日志级别,从低到高的优先级:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
-->
<root level="DEBUG">
<appender-ref ref="spring6log"/>
</root>
</loggers>
<appenders>
<!--输出日志信息到控制台-->
<console name="spring6log" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
</appenders>
</configuration>
第三步:使用日志框架
public class FirstSpringTest {
@Test
public void testBeginInitBean(){
// 注意:不是在调用getBean()方法的时候创建对象,执行以下代码的时候,就会实例化对象。
new ClassPathXmlApplicationContext("spring6.xml");
// 你自己怎么去使用log4j2记录日志信息呢?
// 第一步:创建日志记录器对象
// 获取FirstSpringTest类的日志记录器对象,也就是说只要是FirstSpringTest类中的代码执行记录日志的话,就输出相关的日志信息。
Logger logger = LoggerFactory.getLogger(FirstSpringTest.class);
// 第二步:记录日志,根据不同的级别来输出日志
logger.info("我是一条消息");
logger.debug("我是一条调试信息");
logger.error("我是一条错误信息");
}
}
四、Spring对IoC的实现
4.1 IoC 控制反转
- 控制反转是一种思想。
- 控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则。
- 控制反转,反转的是什么?
- 将对象的创建权利交出去,交给第三方容器负责。
- 将对象和对象之间关系的维护权交出去,交给第三方容器负责。
- 控制反转这种思想如何实现呢?
- DI(Dependency Injection):依赖注入
4.2 依赖注入
依赖注入实现了控制反转的思想。
Spring通过依赖注入的方式来完成Bean管理的。
Bean管理说的是:Bean对象的创建,以及Bean对象中属性的赋值(或者叫做Bean对象之间关系的维护)。
依赖注入:
- 依赖指的是对象和对象之间的关联关系。
- 注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系。
依赖注入常见的实现方式包括两种:
- 第一种:set注入
- 第二种:构造注入
新建模块:spring6-003-dependency-injection
4.2.1 set注入
set注入,基于set方法实现的,底层会通过反射机制调用属性对应的set方法然后给属性赋值。这种方式要求属性必须对外提供set方法。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-002-dependency-injection</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
package com.powernode.spring6.dao;
/**
* @author 动力节点
* @version 1.0
* @className UserDao
* @since 1.0
**/
public class UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
public void insert(){
//System.out.println("数据库正在保存用户信息。");
// 使用一下log4j2日志框架
logger.info("数据库正在保存用户信息。");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
/**
* @author 动力节点
* @version 1.0
* @className UserService
* @since 1.0
**/
public class UserService {
private UserDao userDao;
private VipDao vipDao;
public void setAbc(VipDao vipDao){
this.vipDao = vipDao;
}
// set注入的话,必须提供一个set方法。
// Spring容器会调用这个set方法,来给userDao属性赋值。
// 我自己写一个set方法,不使用IDEA工具生成的。不符合javabean规范。
// 至少这个方法是以set单词开始的。前三个字母不能随便写,必须是“set"
/*public void setMySQLUserDao(UserDao xyz){
this.userDao = xyz;
}*/
// 这个set方法是IDEA工具生成的,符合javabean规范。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void saveUser(){
// 保存用户信息到数据库
userDao.insert();
vipDao.insert();
}
}
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
package com.powernode.spring6.test;
import com.powernode.spring6.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className DITest
* @since 1.0
**/
public class DITest {
@Test
public void testSetDI(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
userService.saveUser();
}
}
运行结果:
重点内容是,什么原理:
<?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">
<!--配置dao-->
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
<!--配置service-->
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<!-- 想让Spring调用对应的set方法,需要配置property标签 -->
<!-- name属性怎么指定值:set方法的方法名,去掉set,然后把剩下的单词首字母变小写,写到这里。-->
<!-- ref翻译为引用。英语单词:references。ref后面指定的是要注入的bean的id。-->
<!--<property name="mySQLUserDao" ref="userDaoBean"/>-->
<!--set方法起名的时候,不要为难自己,按照规范来。所以一般情况下name位置写属性名就行了。-->
<property name="userDao" ref="userDaoBean"/>
<!--<property name="vipDao" ref="vipDaoBean"/>-->
<property name="abc" ref="vipDaoBean"/>
</bean>
<bean id="vipDaoBean" class="com.powernode.spring6.dao.VipDao"/>
</beans>
实现原理:
通过property标签获取到属性名:userDao
通过属性名推断出set方法名:setUserDao
通过反射机制调用setUserDao()方法给属性赋值
property标签的name是属性名。
property标签的ref是要注入的bean对象的id。(通过ref属性来完成bean的装配,这是bean最简单的一种装配方式。装配指的是:创建系统组件之间关联的动作)
可以把set方法注释掉,再测试一下:
通过测试得知,底层实际上调用了setUserDao()方法。所以需要确保这个方法的存在。
我们现在把属性名修改一下,但方法名还是setUserDao(),我们来测试一下:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
/**
* @author 动力节点
* @version 1.0
* @className UserService
* @since 1.0
**/
public class UserService {
private UserDao aaa;
// 使用set方式注入,必须提供set方法。
// 反射机制要调用这个方法给属性赋值的。
public void setUserDao(UserDao userDao) {
this.aaa = userDao;
}
public void save(){
aaa.insert();
}
}
运行测试程序:
通过测试看到程序仍然可以正常执行,说明property标签的name是:setUserDao()方法名演变得到的。演变的规律是:
- setUsername() 演变为 username
- setPassword() 演变为 password
- setUserDao() 演变为 userDao
- setUserService() 演变为 userService
另外,对于property标签来说,ref属性也可以采用标签的方式,但使用ref属性是多数的:
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao">
<ref bean="userDaoBean"/>
</property>
</bean>
总结:set注入的核心实现原理:通过反射机制调用set方法来给属性赋值,让两个对象之间产生关系。
4.2.2 构造注入
核心原理:通过调用构造方法来给属性赋值。
package com.powernode.spring6.dao;
/**
* @author 动力节点
* @version 1.0
* @className OrderDao
* @since 1.0
**/
public class OrderDao {
public void deleteById(){
System.out.println("正在删除订单。。。");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.OrderDao;
/**
* @author 动力节点
* @version 1.0
* @className OrderService
* @since 1.0
**/
public class OrderService {
private OrderDao orderDao;
// 通过反射机制调用构造方法给属性赋值
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
public void delete(){
orderDao.deleteById();
}
}
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--index="0"表示构造方法的第一个参数,将orderDaoBean对象传递给构造方法的第一个参数。-->
<constructor-arg index="0" ref="orderDaoBean"/>
</bean>
@Test
public void testConstructorDI(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderServiceBean = applicationContext.getBean("orderServiceBean", OrderService.class);
orderServiceBean.delete();
}
运行结果如下:
如果构造方法有两个参数:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.OrderDao;
import com.powernode.spring6.dao.UserDao;
/**
* @author 动力节点
* @version 1.0
* @className OrderService
* @since 1.0
**/
public class OrderService {
private OrderDao orderDao;
private UserDao userDao;
// 通过反射机制调用构造方法给属性赋值
public OrderService(OrderDao orderDao, UserDao userDao) {
this.orderDao = orderDao;
this.userDao = userDao;
}
public void delete(){
orderDao.deleteById();
userDao.insert();
}
}
spring配置文件:
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--第一个参数下标是0-->
<constructor-arg index="0" ref="orderDaoBean"/>
<!--第二个参数下标是1-->
<constructor-arg index="1" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
执行测试程序:
不使用参数下标,使用参数的名字可以吗?
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--这里使用了构造方法上参数的名字-->
<constructor-arg name="orderDao" ref="orderDaoBean"/>
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
执行测试程序:
不指定参数下标,不指定参数名字,可以吗?
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--没有指定下标,也没有指定参数名字-->
<!--这种方式实际上是根据类型进行注入的。spring会自动根据类型来判断把ref注入给哪个参数。-->
<constructor-arg ref="orderDaoBean"/>
<constructor-arg ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
执行测试程序:
配置文件中构造方法参数的类型顺序和构造方法参数的类型顺序不一致呢?(spring可以判断匹配)
<bean id="orderDaoBean" class="com.powernode.spring6.dao.OrderDao"/>
<bean id="orderServiceBean" class="com.powernode.spring6.service.OrderService">
<!--顺序已经和构造方法的参数顺序不同了-->
<constructor-arg ref="userDaoBean"/>
<constructor-arg ref="orderDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
执行测试程序:
通过测试得知,通过构造方法注入的时候:
- 可以通过下标
- 可以通过参数名
- 也可以不指定下标和参数名,可以类型自动推断。
Spring在装配方面做的还是比较健壮的。
4.3 set注入专题
4.3.1 注入外部Bean
在之前4.2.1中使用的案例就是注入外部Bean的方式。
<?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="userDaoBean" class="com.powernode.spring6.dao.UserDao"/>
<bean id="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
外部Bean的特点:bean定义到外面,在property标签中使用ref属性进行注入。通常这种方式是常用。
4.3.2 注入内部Bean
内部Bean的方式:在bean标签中嵌套bean标签。
<?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="userServiceBean" class="com.powernode.spring6.service.UserService">
<property name="userDao">
<bean class="com.powernode.spring6.dao.UserDao"/>
</property>
</bean>
</beans>
@Test
public void testInnerBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-inner-bean.xml");
UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
userService.save();
}
执行测试程序:
这种方式作为了解。
4.3.3 注入简单类型
我们之前在进行注入的时候,对象的属性是另一个对象。
public class UserService{
private UserDao userDao;
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
}
那如果对象的属性是int类型呢?
public class User{
private int age;
public void setAge(int age){
this.age = age;
}
}
可以通过set注入的方式给该属性赋值吗?
- 当然可以。因为只要能够调用set方法就可以给属性赋值。
编写程序给一个User对象的age属性赋值20:
第一步:定义User类,提供age属性,提供age属性的setter方法。
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User {
private int age;
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
'}';
}
}
第二步:编写spring配置文件:spring-simple-type.xml
<?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="userBean" class="com.powernode.spring6.beans.User">
<!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref-->
<!--<property name="age" value="20"/>-->
<property name="age">
<value>20</value>
</property>
</bean>
</beans>
第三步:编写测试程序
@Test
public void testSimpleType(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-simple-type.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}
第四步:运行测试程序
需要特别注意:如果给简单类型赋值,使用value属性或value标签。而不是ref。
简单类型包括哪些呢?可以通过Spring的源码来分析一下:BeanUtils类
public class BeanUtils{
//.......
/**
* Check if the given type represents a "simple" property: a simple value
* type or an array of simple value types.
* <p>See {@link #isSimpleValueType(Class)} for the definition of <em>simple
* value type</em>.
* <p>Used to determine properties to check for a "simple" dependency-check.
* @param type the type to check
* @return whether the given type represents a "simple" property
* @see org.springframework.beans.factory.support.RootBeanDefinition#DEPENDENCY_CHECK_SIMPLE
* @see org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#checkDependencies
* @see #isSimpleValueType(Class)
*/
public static boolean isSimpleProperty(Class<?> type) {
Assert.notNull(type, "'type' must not be null");
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
}
/**
* Check if the given type represents a "simple" value type: a primitive or
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
* <p>{@code Void} and {@code void} are not considered simple value types.
* @param type the type to check
* @return whether the given type represents a "simple" value type
* @see #isSimpleProperty(Class)
*/
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) || //java.util.Date是简单类型
Temporal.class.isAssignableFrom(type) || //Temporal是Java8提供的时间和时区类型
URI.class == type ||
URL.class == type ||
Locale.class == type || //Locale是语言类,也是简单类型。
Class.class == type));
}
//........
}
通过源码分析得知,简单类型包括:
- 基本数据类型
- 基本数据类型对应的包装类
- String或其他的CharSequence子类
- Number子类
- Date子类
- Enum子类
- URI
- URL
- Temporal子类
- Locale
- Class
- 另外还包括以上简单值类型对应的数组类型。
经典案例:给数据源的属性注入值:
假设我们现在要自己手写一个数据源,我们都知道所有的数据源都要实现javax.sql.DataSource接口,并且数据源中应该有连接数据库的信息,例如:driver、url、username、password等。
package com.powernode.spring6.beans;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 所有的数据源都要实现java规范:javax.sql.DataSource
* 什么是数据源:能够给你提供Connection对象的,都是数据源。
* @className MyDataSource
* @since 1.0
**/
public class MyDataSource implements DataSource {
private String driver;
private String url;
private String username;
private String password;
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
@Override
public Connection getConnection() throws SQLException {
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
我们给driver、url、username、password四个属性分别提供了setter方法,我们可以使用spring的依赖注入完成数据源对象的创建和属性的赋值吗?看配置文件
<?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="dataSource" class="com.powernode.spring6.beans.MyDataSource">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
</beans>
测试程序:
@Test
public void testDataSource(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-datasource.xml");
MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);
System.out.println(dataSource);
}
执行测试程序:
你学会了吗?
接下来,我们编写一个程序,把所有的简单类型全部测试一遍:
编写一个类A:
package com.powernode.spring6.beans;
import java.net.URI;
import java.net.URL;
import java.time.LocalDate;
import java.util.Date;
import java.util.Locale;
/**
* @author 动力节点
* @version 1.0
* @className A
* @since 1.0
**/
public class A {
private byte b;
private short s;
private int i;
private long l;
private float f;
private double d;
private boolean flag;
private char c;
private Byte b1;
private Short s1;
private Integer i1;
private Long l1;
private Float f1;
private Double d1;
private Boolean flag1;
private Character c1;
private String str;
private Date date;
private Season season;
private URI uri;
private URL url;
private LocalDate localDate;
private Locale locale;
private Class clazz;
// 生成setter方法
// 生成toString方法
}
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
<?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="a" class="com.powernode.spring6.beans.A">
<property name="b" value="1"/>
<property name="s" value="1"/>
<property name="i" value="1"/>
<property name="l" value="1"/>
<property name="f" value="1"/>
<property name="d" value="1"/>
<property name="flag" value="false"/>
<property name="c" value="a"/>
<property name="b1" value="2"/>
<property name="s1" value="2"/>
<property name="i1" value="2"/>
<property name="l1" value="2"/>
<property name="f1" value="2"/>
<property name="d1" value="2"/>
<property name="flag1" value="true"/>
<property name="c1" value="a"/>
<property name="str" value="zhangsan"/>
<!--注意:value后面的日期字符串格式不能随便写,必须是Date对象toString()方法执行的结果。-->
<!--如果想使用其他格式的日期字符串,就需要进行特殊处理了。具体怎么处理,可以看后面的课程!!!!-->
<!--在实际开发中,我们一般不会把Date当做简单类型,虽然它是简单类型。一般会采用ref给Date类型的属性赋值。-->
<property name="date" value="Fri Sep 30 15:26:38 CST 2022"/>
<property name="season" value="WINTER"/>
<property name="uri" value="/save.do"/>
<!--spring6之后,会自动检查url是否有效,如果无效会报错。-->
<property name="url" value="http://www.baidu.com"/>
<property name="localDate" value="EPOCH"/>
<!--java.util.Locale 主要在软件的本地化时使用。它本身没有什么功能,更多的是作为一个参数辅助其他方法完成输出的本地化。-->
<property name="locale" value="CHINESE"/>
<property name="clazz" value="java.lang.String"/>
</bean>
</beans>
编写测试程序:
@Test
public void testAllSimpleType(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-all-simple-type.xml");
A a = applicationContext.getBean("a", A.class);
System.out.println(a);
}
执行结果如下:
需要注意的是:
- 如果把Date当做简单类型的话,日期字符串格式不能随便写。格式必须符合Date的toString()方法格式。显然这就比较鸡肋了。如果我们提供一个这样的日期字符串:2010-10-11,在这里是无法赋值给Date类型的属性的。
- spring6之后,当注入的是URL,那么这个url字符串是会进行有效性检测的。如果是一个存在的url,那就没问题。如果不存在则报错。
4.3.4 级联属性赋值(了解)
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className Clazz
* @since 1.0
**/
public class Clazz {
private String name;
public Clazz() {
}
public Clazz(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Clazz{" +
"name='" + name + '\'' +
'}';
}
}
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className Student
* @since 1.0
**/
public class Student {
private String name;
private Clazz clazz;
public Student() {
}
public Student(String name, Clazz clazz) {
this.name = name;
this.clazz = clazz;
}
public void setName(String name) {
this.name = name;
}
public void setClazz(Clazz clazz) {
this.clazz = clazz;
}
public Clazz getClazz() {
return clazz;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", clazz=" + clazz +
'}';
}
}
<?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="clazzBean" class="com.powernode.spring6.beans.Clazz"/>
<bean id="student" class="com.powernode.spring6.beans.Student">
<property name="name" value="张三"/>
<!--要点1:以下两行配置的顺序不能颠倒-->
<property name="clazz" ref="clazzBean"/>
<!--要点2:clazz属性必须有getter方法-->
<property name="clazz.name" value="高三一班"/>
</bean>
</beans>
@Test
public void testCascade(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-cascade.xml");
Student student = applicationContext.getBean("student", Student.class);
System.out.println(student);
}
运行结果:
要点:
- 在spring配置文件中,如上,注意顺序。
- 在spring配置文件中,clazz属性必须提供getter方法。
4.3.5 注入数组
当数组中的元素是简单类型:
package com.powernode.spring6.beans;
import java.util.Arrays;
public class Person {
private String[] favariteFoods;
public void setFavariteFoods(String[] favariteFoods) {
this.favariteFoods = favariteFoods;
}
@Override
public String toString() {
return "Person{" +
"favariteFoods=" + Arrays.toString(favariteFoods) +
'}';
}
}
<?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="person" class="com.powernode.spring6.beans.Person">
<!--这个数组属性当中的元素类型是String简单类型-->
<property name="favariteFoods">
<array>
<value>鸡排</value>
<value>汉堡</value>
<value>鹅肝</value>
</array>
</property>
</bean>
</beans>
@Test
public void testArraySimple(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array-simple.xml");
Person person = applicationContext.getBean("person", Person.class);
System.out.println(person);
}
当数组中的元素是非简单类型:一个订单中包含多个商品。
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className Goods
* @since 1.0
**/
public class Goods {
private String name;
public Goods() {
}
public Goods(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
'}';
}
}
package com.powernode.spring6.beans;
import java.util.Arrays;
/**
* @author 动力节点
* @version 1.0
* @className Order
* @since 1.0
**/
public class Order {
// 一个订单中有多个商品
private Goods[] goods;
public Order() {
}
public Order(Goods[] goods) {
this.goods = goods;
}
public void setGoods(Goods[] goods) {
this.goods = goods;
}
@Override
public String toString() {
return "Order{" +
"goods=" + Arrays.toString(goods) +
'}';
}
}
<?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="goods1" class="com.powernode.spring6.beans.Goods">
<property name="name" value="西瓜"/>
</bean>
<bean id="goods2" class="com.powernode.spring6.beans.Goods">
<property name="name" value="苹果"/>
</bean>
<bean id="order" class="com.powernode.spring6.beans.Order">
<!--这个数组当中的类型就不是简单类型了-->
<property name="goods">
<array>
<!--这里使用ref标签即可-->
<ref bean="goods1"/>
<ref bean="goods2"/>
</array>
</property>
</bean>
</beans>
测试程序:
@Test
public void testArray(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array.xml");
Order order = applicationContext.getBean("order", Order.class);
System.out.println(order);
}
执行结果:
要点:
- 如果数组中是简单类型,使用value标签。
- 如果数组中是非简单类型,使用ref标签。
4.3.6 注入List集合
List集合:有序可重复
package com.powernode.spring6.beans;
import java.util.List;
/**
* @author 动力节点
* @version 1.0
* @className People
* @since 1.0
**/
public class People {
// 一个人有多个名字
private List<String> names;
public void setNames(List<String> names) {
this.names = names;
}
@Override
public String toString() {
return "People{" +
"names=" + names +
'}';
}
}
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="names">
<!--list集合有序可重复-->
<list>
<value>铁锤</value>
<value>张三</value>
<value>张三</value>
<value>张三</value>
<value>狼</value>
</list>
</property>
</bean>
</beans>
@Test
public void testCollection(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-collection.xml");
People peopleBean = applicationContext.getBean("peopleBean", People.class);
System.out.println(peopleBean);
}
执行结果:
注意:注入List集合的时候使用list标签,如果List集合中是简单类型使用value标签,反之使用ref标签。
4.3.7 注入Set集合
Set集合:无序不可重复
package com.powernode.spring6.beans;
import java.util.List;
import java.util.Set;
/**
* @author 动力节点
* @version 1.0
* @className People
* @since 1.0
**/
public class People {
// 一个人有多个电话
private Set<String> phones;
public void setPhones(Set<String> phones) {
this.phones = phones;
}
//......
@Override
public String toString() {
return "People{" +
"phones=" + phones +
", names=" + names +
'}';
}
}
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="phones">
<!--set集合无序不可重复-->
<set>
<!--非简单类型可以使用ref,简单类型使用value-->
<value>110</value>
<value>110</value>
<value>120</value>
<value>120</value>
<value>119</value>
<value>119</value>
</set>
</property>
</bean>
</beans>
执行结果:
要点:
- 使用<set>标签
- set集合中元素是简单类型的使用value标签,反之使用ref标签。
4.3.8 注入Map集合
package com.powernode.spring6.beans;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author 动力节点
* @version 1.0
* @className People
* @since 1.0
**/
public class People {
// 一个人有多个住址
private Map<Integer, String> addrs;
public void setAddrs(Map<Integer, String> addrs) {
this.addrs = addrs;
}
//......
@Override
public String toString() {
return "People{" +
"addrs=" + addrs +
", phones=" + phones +
", names=" + names +
'}';
}
}
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="addrs">
<!--注入Map集合-->
<map>
<!--如果key不是简单类型,使用 key-ref 属性-->
<!--如果value不是简单类型,使用 value-ref 属性-->
<entry key="1" value="北京大兴区"/>
<entry key="2" value="上海浦东区"/>
<entry key="3" value="深圳宝安区"/>
</map>
</property>
</bean>
</beans>
执行结果:
要点:
- 使用<map>标签
- 如果key是简单类型,使用 key 属性,反之使用 key-ref 属性。
- 如果value是简单类型,使用 value 属性,反之使用 value-ref 属性。
4.3.9 注入Properties
java.util.Properties继承java.util.Hashtable,所以Properties也是一个Map集合。
package com.powernode.spring6.beans;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* @author 动力节点
* @version 1.0
* @className People
* @since 1.0
**/
public class People {
// 注入属性类对象
// Properties本质上也是一个Map集合。
// Properties的父类Hashtable,Hashtable实现了Map接口。
// 虽然这个也是一个Map集合,但是和Map的注入方式有点像,但是不同。
// Properties的key和value只能是String类型。
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
//......
@Override
public String toString() {
return "People{" +
"properties=" + properties +
", addrs=" + addrs +
", phones=" + phones +
", names=" + names +
'}';
}
}
<?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="peopleBean" class="com.powernode.spring6.beans.People">
<property name="properties">
<!--注入Properties属性类对象-->
<props>
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>
</beans>
执行测试程序:
要点:
- 使用<props>标签嵌套<prop>标签完成。
4.3.10 注入null和空字符串
注入空字符串使用:<value/> 或者 value=“”
注入null使用:<null/> 或者 不为该属性赋值
- 我们先来看一下,怎么注入空字符串。
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className Vip
* @since 1.0
**/
public class Vip {
private String email;
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Vip{" +
"email='" + email + '\'' +
'}';
}
}
<?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="vipBean" class="com.powernode.spring6.beans.Vip">
<!--空串的第一种方式-->
<!--<property name="email" value=""/>-->
<!--空串的第二种方式-->
<property name="email">
<value/>
</property>
</bean>
</beans>
@Test
public void testNull(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-null.xml");
Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vipBean);
}
执行结果:
- 怎么注入null呢?
第一种方式:不给属性赋值
<?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="vipBean" class="com.powernode.spring6.beans.Vip" />
</beans>
执行结果:
第二种方式:使用<null/>
<?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="vipBean" class="com.powernode.spring6.beans.Vip">
<property name="email">
<null/>
</property>
</bean>
</beans>
执行结果:
4.3.11 注入的值中含有特殊符号
XML中有5个特殊字符,分别是:<、>、'、"、&
以上5个特殊符号在XML中会被特殊对待,会被当做XML语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。
解决方案包括两种:
- 第一种:特殊符号使用转义字符代替。
- 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。
5个特殊字符对应的转义字符分别是:
特殊字符 | 转义字符 |
---|---|
> | > |
< | < |
’ | ' |
" | " |
& | & |
先使用转义字符来代替:
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className Math
* @since 1.0
**/
public class Math {
private String result;
public void setResult(String result) {
this.result = result;
}
@Override
public String toString() {
return "Math{" +
"result='" + result + '\'' +
'}';
}
}
<?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="mathBean" class="com.powernode.spring6.beans.Math">
<!--第一种方案:使用实体符号代替特殊符号-->
<property name="result" value="2 < 3"/>
</bean>
</beans>
@Test
public void testSpecial(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-special.xml");
Math mathBean = applicationContext.getBean("mathBean", Math.class);
System.out.println(mathBean);
}
执行结果:
我们再来使用CDATA方式:
<?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="mathBean" class="com.powernode.spring6.beans.Math">
<!--第二种方案:使用<![CDATA[]]>-->
<property name="result">
<!--只能使用value标签-->
<value><![CDATA[2 < 3]]></value>
</property>
</bean>
</beans>
注意:使用CDATA时,不能使用value属性,只能使用value标签。
执行结果:
4.4 p命名空间注入
目的:简化配置。
使用p命名空间注入的前提条件包括两个:
- 第一:在XML头部信息中添加p命名空间的配置信息:xmlns:p="http://www.springframework.org/schema/p"
- 第二:p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法。
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className Customer
* @since 1.0
**/
public class Customer {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Customer{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
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">
<!--
第一步:在spring的配置文件头部添加p命名空间。xmlns:p="http://www.springframework.org/schema/p"
第二步:使用 p:属性名 = "属性值"
-->
<bean id="customerBean" class="com.powernode.spring6.beans.Customer" p:name="zhangsan" p:age="20"/>
</beans>
@Test
public void testP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-p.xml");
Customer customerBean = applicationContext.getBean("customerBean", Customer.class);
System.out.println(customerBean);
}
执行结果:
把setter方法去掉:
所以p命名空间实际上是对set注入的简化。
4.5 c命名空间注入
c命名空间是简化构造方法注入的。
使用c命名空间的两个前提条件:
第一:需要在xml配置文件头部添加信息:xmlns:c=“http://www.springframework.org/schema/c”
第二:需要提供构造方法。
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className MyTime
* @since 1.0
**/
public class MyTime {
private int year;
private int month;
private int day;
public MyTime(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public String toString() {
return "MyTime{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:c="http://www.springframework.org/schema/c"
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">
<!--
第一步:在spring的配置文件头部添加: xmlns:c="http://www.springframework.org/schema/c"
第二步:使用
c:_0 下标方式
c:name 参数名方式
-->
<!--<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:year="1970" c:month="1" c:day="1"/>-->
<bean id="myTimeBean" class="com.powernode.spring6.beans.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>
</beans>
@Test
public void testC(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-c.xml");
MyTime myTimeBean = applicationContext.getBean("myTimeBean", MyTime.class);
System.out.println(myTimeBean);
}
执行结果:
把构造方法注释掉:
所以,c命名空间是依靠构造方法的。
注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。
4.6 util命名空间
使用util命名空间可以让配置复用。
使用util命名空间的前提是:在spring配置文件头部添加配置信息。如下:
package com.powernode.spring6.beans;
import java.util.Properties;
/**
* @author 动力节点
* @version 1.0
* @className MyDataSource1
* @since 1.0
**/
public class MyDataSource1 {
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "MyDataSource1{" +
"properties=" + properties +
'}';
}
}
package com.powernode.spring6.beans;
import java.util.Properties;
/**
* @author 动力节点
* @version 1.0
* @className MyDataSource2
* @since 1.0
**/
public class MyDataSource2 {
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}
@Override
public String toString() {
return "MyDataSource2{" +
"properties=" + properties +
'}';
}
}
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<!--引入util命名空间
在spring的配置文件头部添加:
xmlns:util="http://www.springframework.org/schema/util"
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
-->
<util:properties id="prop">
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</util:properties>
<bean id="dataSource1" class="com.powernode.spring6.beans.MyDataSource1">
<property name="properties" ref="prop"/>
</bean>
<bean id="dataSource2" class="com.powernode.spring6.beans.MyDataSource2">
<property name="properties" ref="prop"/>
</bean>
</beans>
@Test
public void testUtil(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-util.xml");
MyDataSource1 dataSource1 = applicationContext.getBean("dataSource1", MyDataSource1.class);
System.out.println(dataSource1);
MyDataSource2 dataSource2 = applicationContext.getBean("dataSource2", MyDataSource2.class);
System.out.println(dataSource2);
}
执行结果:
4.7 基于XML的自动装配
Spring还可以完成自动化的注入,自动化注入又被称为自动装配。它可以根据名字进行自动装配,也可以根据类型进行自动装配。
4.7.1 根据名称自动装配
package com.powernode.spring6.dao;
/**
* @author 动力节点
* @version 1.0
* @className UserDao
* @since 1.0
**/
public class UserDao {
public void insert(){
System.out.println("正在保存用户数据。");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
/**
* @author 动力节点
* @version 1.0
* @className UserService
* @since 1.0
**/
public class UserService {
private UserDao aaa;
// 这个set方法非常关键
public void setAaa(UserDao aaa) {
this.aaa = aaa;
}
public void save(){
aaa.insert();
}
}
Spring的配置文件这样配置:
<?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">
<!--根据名字进行自动装配-->
<!--注意:自动装配也是基于set方式实现的。-->
<bean id="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/>
<!--id一般也叫作bean的名称。-->
<!--根据名字进行自动装配的时候,被注入的对象的bean的id不能随便写,怎么写?set方法的方法名去掉set,剩下单词首字母小写。-->
<bean id="aaa" class="com.powernode.spring6.dao.UserDao"/>
</beans>
这个配置起到关键作用:
- UserService Bean中需要添加autowire=“byName”,表示通过名称进行装配。
- UserService类中有一个UserDao属性,而UserDao属性的名字是aaa,对应的set方法是setAaa(),正好和UserDao Bean的id是一样的。这就是根据名称自动装配。
@Test
public void testAutowireByName(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}
执行结果:
我们来测试一下,byName装配是和属性名有关还是和set方法名有关系:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
/**
* @author 动力节点
* @version 1.0
* @className UserService
* @since 1.0
**/
public class UserService {
// 这里没修改
private UserDao aaa;
/*public void setAaa(UserDao aaa) {
this.aaa = aaa;
}*/
// set方法名变化了
public void setDao(UserDao aaa){
this.aaa = aaa;
}
public void save(){
aaa.insert();
}
}
在执行测试程序:
通过测试得知,aaa属性并没有赋值成功。也就是并没有装配成功。
我们将spring配置文件修改以下:
<?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="userService" class="com.powernode.spring6.service.UserService" autowire="byName"/>
<!--这个id修改了-->
<bean id="dao" class="com.powernode.spring6.dao.UserDao"/>
</beans>
执行测试程序:
这说明,如果根据名称装配(byName),底层会调用set方法进行注入。
例如:setAge() 对应的名字是age,setPassword()对应的名字是password,setEmail()对应的名字是email。
4.7.2 根据类型自动装配
package com.powernode.spring6.dao;
/**
* @author 动力节点
* @version 1.0
* @className AccountDao
* @since 1.0
**/
public class AccountDao {
public void insert(){
System.out.println("正在保存账户信息");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.AccountDao;
/**
* @author 动力节点
* @version 1.0
* @className AccountService
* @since 1.0
**/
public class AccountService {
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void save(){
accountDao.insert();
}
}
<?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">
<!--byType表示根据类型自动装配-->
<!--自动装配是基于set方法的-->
<!--根据类型进行自动装配的时候,在有效的配置文件当中,某种类型的实例只能有一个。-->
<bean id="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/>
<bean class="com.powernode.spring6.dao.AccountDao"/>
</beans>
@Test
public void testAutowireByType(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
accountService.save();
}
执行结果:
我们把AccountService中的set方法注释掉,再执行:
可以看到无论是byName还是byType,在装配的时候都是基于set方法的。所以set方法是必须要提供的。提供构造方法是不行的,大家可以测试一下。这里就不再赘述。
如果byType,根据类型装配时,如果配置文件中有两个类型一样的bean会出现什么问题呢?
<?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="accountService" class="com.powernode.spring6.service.AccountService" autowire="byType"/>
<bean id="x" class="com.powernode.spring6.dao.AccountDao"/>
<bean id="y" class="com.powernode.spring6.dao.AccountDao"/>
</beans>
执行测试程序:
测试结果说明了,当byType进行自动装配的时候,配置文件中某种类型的Bean必须是唯一的,不能出现多个。
4.8 Spring引入外部属性配置文件
我们都知道编写数据源的时候是需要连接数据库的信息的,例如:driver url username password等信息。这些信息可以单独写到一个属性配置文件中吗,这样用户修改起来会更加的方便。当然可以。
第一步:写一个数据源类,提供相关属性。
package com.powernode.spring6.beans;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* @author 动力节点
* @version 1.0
* @className MyDataSource
* @since 1.0
**/
public class MyDataSource implements DataSource {
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
private String driver;
private String url;
private String username;
private String password;
public void setDriver(String driver) {
this.driver = driver;
}
public void setUrl(String url) {
this.url = url;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
//......
}
第二步:在类路径下新建jdbc.properties文件,并配置信息。
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root123
第三步:在spring配置文件中引入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">
</beans>
第四步:在spring中配置使用jdbc.properties文件。
<?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">
<!--
引入外部的properties文件
第一步:引入context命名空间。
第二步:使用标签context:property-placeholder的location属性来指定属性配置文件的路径。
location默认从类的根路径下开始加载资源。
-->
<context:property-placeholder location="jdbc.properties"/>
<bean id="dataSource" class="com.powernode.spring6.beans.MyDataSource">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>
</beans>
测试程序:
@Test
public void testProperties(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-properties.xml");
MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);
System.out.println(dataSource);
}
执行结果:
五、Bean的作用域
5.1 singleton
默认情况下,Spring的IoC容器创建的Bean对象是单例的。来测试一下:
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className SpringBean
* @since 1.0
**/
public class SpringBean {
}
<?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">
<!--
目前来说:scope属性有两个值
第一个值:singleton 单例(默认情况下就是单例的。)
第二个值:prototype 原型/多例
-->
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" />
</beans>
@Test
public void testScope(){
/**
* 1. Spring默认情况下是如何管理这个Bean的:
* 默认情况下Bean是单例的。(单例:singleton)
* 在Spring上下文初始化的时候实例化。
* 每一次调用getBean()方法的时候,都返回那个单例的对象。
*
* 2. 当将bean的scope属性设置为prototype:
* bean是多例的。
* spring上下文初始化的时候,并不会初始化这些prototype的bean。
* 每一次调用getBean()方法的时候,实例化该bean对象。
* prototype翻译为:原型。
*/
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}
执行结果:
通过测试得知:Spring的IoC容器中,默认情况下,Bean对象是单例的。
这个对象在什么时候创建的呢?可以为SpringBean提供一个无参数构造方法,测试一下,如下:
package com.powernode.spring6.beans;
/**
* @author 动力节点
* @version 1.0
* @className SpringBean
* @since 1.0
**/
public class SpringBean {
public SpringBean() {
System.out.println("SpringBean的无参数构造方法执行。");
}
}
将测试程序中getBean()所在行代码注释掉:
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
}
执行测试程序:
通过测试得知,默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。
5.2 prototype
如果想让Spring的Bean对象以多例的形式存在,可以在bean标签中指定scope属性的值为:prototype,这样Spring会在每一次执行getBean()方法的时候创建Bean对象,调用几次则创建几次。
<?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="sb" class="com.powernode.spring6.beans.SpringBean" scope="prototype" />
</beans>
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}
执行结果:
我们可以把测试代码中的getBean()方法所在行代码注释掉:
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
}
执行结果:
可以看到这一次在初始化Spring上下文的时候,并没有创建Bean对象。
那你可能会问:scope如果没有配置,它的默认值是什么呢?默认值是singleton,单例的。
<?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="sb" class="com.powernode.spring6.beans.SpringBean" scope="singleton" />
</beans>
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}
执行结果:
通过测试得知,没有指定scope属性时,默认是singleton单例的。
5.3 其它scope
scope属性的值不止两个,它一共包括8个选项:
- singleton:默认的,单例。
- prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
- request:一个请求对应一个Bean。仅限于在WEB应用中使用。
- session:一个会话对应一个Bean。仅限于在WEB应用中使用。
- global session:portlet应用中专用的。 如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
- application:一个应用对应一个Bean。仅限于在WEB应用中使用。
- websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
- 自定义scope:很少使用。
接下来咱们自定义一个Scope,线程级别的Scope,在同一个线程中,获取的Bean都是同一个。跨线程则是不同的对象:(以下内容作为了解)
- 第一步:自定义Scope。(实现Scope接口)
- spring内置了线程范围的类:org.springframework.context.support.SimpleThreadScope,可以直接用。
- 第二步:将自定义的Scope注册到Spring容器中。
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="myThread">
<!--这个Scope接口的实现类使用的是Spring框架内置的。也可以自定义。-->
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
- 第三步:使用Scope。
<bean id="sb" class="com.powernode.spring6.beans.SpringBean" scope="myThread" />
编写测试程序:
@Test
public void testCustomScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
System.out.println(sb2);
// 启动线程
new Thread(new Runnable() {
@Override
public void run() {
SpringBean a = applicationContext.getBean("sb", SpringBean.class);
SpringBean b = applicationContext.getBean("sb", SpringBean.class);
System.out.println(a);
System.out.println(b);
}
}).start();
}
执行结果:
六、GoF之工厂模式
- 设计模式:一种可以被重复利用的解决方案。
- GoF(Gang of Four),中文名——四人组。
- 《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。
- 该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。
- 不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。
- GoF23种设计模式可分为三大类:
- 创建型(5个):解决对象创建问题。
- 单例模式
- 工厂方法模式
- 抽象工厂模式
- 建造者模式
- 原型模式
- 结构型(7个):一些类或对象组合在一起的经典结构。
- 代理模式
- 装饰模式
- 适配器模式
- 组合模式
- 享元模式
- 外观模式
- 桥接模式
- 行为型(11个):解决类或对象之间的交互问题。
- 策略模式
- 模板方法模式
- 责任链模式
- 观察者模式
- 迭代子模式
- 命令模式
- 备忘录模式
- 状态模式
- 访问者模式
- 中介者模式
- 解释器模式
- 创建型(5个):解决对象创建问题。
- 工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式。
6.1 工厂模式的三种形态
工厂模式通常有三种形态:
- 第一种:简单工厂模式(Simple Factory):不属于23种设计模式之一。简单工厂模式又叫做:静态 工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
- 第二种:工厂方法模式(Factory Method):是23种设计模式之一。
- 第三种:抽象工厂模式(Abstract Factory):是23种设计模式之一。
6.2 简单工厂模式
简单工厂模式的角色包括三个:
- 抽象产品 角色
- 具体产品 角色
- 工厂类 角色
简单工厂模式的代码如下:
抽象产品角色:
package com.powernode.factory;
/**
* 武器(抽象产品角色)
* @author 动力节点
* @version 1.0
* @className Weapon
* @since 1.0
**/
public abstract class Weapon {
/**
* 所有的武器都有攻击行为
*/
public abstract void attack();
}
具体产品角色:
package com.powernode.factory;
/**
* 坦克(具体产品角色)
* @author 动力节点
* @version 1.0
* @className Tank
* @since 1.0
**/
public class Tank extends Weapon{
@Override
public void attack() {
System.out.println("坦克开炮!");
}
}
package com.powernode.factory;
/**
* 战斗机(具体产品角色)
* @author 动力节点
* @version 1.0
* @className Fighter
* @since 1.0
**/
public class Fighter extends Weapon{
@Override
public void attack() {
System.out.println("战斗机投下原子弹!");
}
}
package com.powernode.factory;
/**
* 匕首(具体产品角色)
* @author 动力节点
* @version 1.0
* @className Dagger
* @since 1.0
**/
public class Dagger extends Weapon{
@Override
public void attack() {
System.out.println("砍他丫的!");
}
}
工厂类角色:
package com.powernode.factory;
/**
* 工厂类角色
* @author 动力节点
* @version 1.0
* @className WeaponFactory
* @since 1.0
**/
public class WeaponFactory {
/**
* 静态方法。要获取什么产品?就看你传什么参数,传TANK获取坦克,传DAGGER获取匕首,传FIGHTER获取战斗机
* 简单工厂模式中有一个静态方法,所以被称为:静态工厂方法模式。
* @param weaponType
* @return
*/
/**
* 根据不同的武器类型生产武器
* @param weaponType 武器类型
* @return 武器对象
*/
public static Weapon get(String weaponType){
if (weaponType == null || weaponType.trim().length() == 0) {
return null;
}
Weapon weapon = null;
if ("TANK".equals(weaponType)) {
weapon = new Tank();
} else if ("FIGHTER".equals(weaponType)) {
weapon = new Fighter();
} else if ("DAGGER".equals(weaponType)) {
weapon = new Dagger();
} else {
throw new RuntimeException("不支持该武器!");
}
return weapon;
}
}
测试程序(客户端程序):
package com.powernode.factory;
/**
* @author 动力节点
* @version 1.0
* @className Client
* @since 1.0
**/
public class Client {
public static void main(String[] args) {
// 需要坦克
// 对于我客户端来说,坦克的生产细节,我不需要关心,我只需要向工厂索要即可。
// 简单工厂模式达到了什么呢?职责分离。客户端不需要关心产品的生产细节。
// 客户端只负责消费。工厂类负责生产。一个负责生产,一个负责消费。生产者和消费者分离了。这就是简单工厂模式的作用。
Weapon weapon1 = WeaponFactory.get("TANK");
weapon1.attack();
// 需要战斗机
Weapon weapon2 = WeaponFactory.get("FIGHTER");
weapon2.attack();
// 需要匕首
Weapon weapon3 = WeaponFactory.get("DAGGER");
weapon3.attack();
}
}
执行结果:
简单工厂模式的优点:
- 客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。客户端只负责“消费”,工厂负责“生产”。生产和消费分离。
简单工厂模式的缺点:
- 缺点1:工厂类集中了所有产品的创造逻辑,形成一个无所不知的全能类,有人把它叫做上帝类。显然工厂类非常关键,不能出问题,一旦出问题,整个系统瘫痪。
- 缺点2:不符合OCP开闭原则,在进行系统扩展时,需要修改工厂类。
Spring中的BeanFactory就使用了简单工厂模式。
设计模式之:简单工厂模式 Simple Factory Pattern
1. 简单工厂模式是工厂方法模式的一种特殊实现,又被称为:静态工厂方法模式。
2. 简单工厂模式解决什么问题呢?
优点:客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。
客户端只负责“消费”,工厂负责“生产”。生产和消费分离。
3. 简单工厂模式中的角色:
* 抽象产品角色
* 具体产品角色
* 工厂类角色
4. 简单工厂模式的缺点?
缺点一:假设现在需要扩展一个新的产品,WeaponFactory工厂类的代码是需要修改的,显然违背了OCP原则。
缺点二:工厂类的责任比较重大,不能出现任何问题,因为这个工厂类负责所有产品的生产,称为全能类,或者有人把它叫做上帝类。
这个工厂类一旦出问题,整个系统必然全部瘫痪。(不要把所有鸡蛋放到一个篮子里面哦。)
6.3 工厂方法模式
工厂方法模式既保留了简单工厂模式的优点,同时又解决了简单工厂模式的缺点。
工厂方法模式的角色包括:
- 抽象工厂角色
- 具体工厂角色
- 抽象产品角色
- 具体产品角色
代码如下:
package com.powernode.factory;
/**
* 武器类(抽象产品角色)
* @author 动力节点
* @version 1.0
* @className Weapon
* @since 1.0
**/
public abstract class Weapon {
/**
* 所有武器都有攻击行为
*/
public abstract void attack();
}
package com.powernode.factory;
/**
* 具体产品角色
* @author 动力节点
* @version 1.0
* @className Gun
* @since 1.0
**/
public class Gun extends Weapon{
@Override
public void attack() {
System.out.println("开枪射击!");
}
}
package com.powernode.factory;
/**
* 具体产品角色
* @author 动力节点
* @version 1.0
* @className Fighter
* @since 1.0
**/
public class Fighter extends Weapon{
@Override
public void attack() {
System.out.println("战斗机发射核弹!");
}
}
package com.powernode.factory;
/**
* 武器工厂接口(抽象工厂角色)
* @author 动力节点
* @version 1.0
* @className WeaponFactory
* @since 1.0
**/
public interface WeaponFactory {
Weapon get();
}
package com.powernode.factory;
/**
* 具体工厂角色
* @author 动力节点
* @version 1.0
* @className GunFactory
* @since 1.0
**/
public class GunFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Gun();
}
}
package com.powernode.factory;
/**
* 具体工厂角色
* @author 动力节点
* @version 1.0
* @className FighterFactory
* @since 1.0
**/
public class FighterFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Fighter();
}
}
客户端程序:
package com.powernode.factory;
/**
* @author 动力节点
* @version 1.0
* @className Client
* @since 1.0
**/
public class Client {
public static void main(String[] args) {
WeaponFactory factory = new GunFactory();
Weapon weapon = factory.get();
weapon.attack();
WeaponFactory factory1 = new FighterFactory();
Weapon weapon1 = factory1.get();
weapon1.attack();
}
}
执行客户端程序:
如果想扩展一个新的产品,只要新增一个产品类,再新增一个该产品对应的工厂即可,例如新增:匕首
package com.powernode.factory;
/**
* @author 动力节点
* @version 1.0
* @className Dagger
* @since 1.0
**/
public class Dagger extends Weapon{
@Override
public void attack() {
System.out.println("砍丫的!");
}
}
package com.powernode.factory;
/**
* @author 动力节点
* @version 1.0
* @className DaggerFactory
* @since 1.0
**/
public class DaggerFactory implements WeaponFactory{
@Override
public Weapon get() {
return new Dagger();
}
}
客户端程序:
package com.powernode.factory;
/**
* @author 动力节点
* @version 1.0
* @className Client
* @since 1.0
**/
public class Client {
public static void main(String[] args) {
WeaponFactory factory = new GunFactory();
Weapon weapon = factory.get();
weapon.attack();
WeaponFactory factory1 = new FighterFactory();
Weapon weapon1 = factory1.get();
weapon1.attack();
WeaponFactory factory2 = new DaggerFactory();
Weapon weapon2 = factory2.get();
weapon2.attack();
}
}
执行结果如下:
我们可以看到在进行功能扩展的时候,不需要修改之前的源代码,显然工厂方法模式符合OCP原则。
工厂方法模式的优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
工厂方法模式的缺点:
- 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
工厂方法模式:Factory Method Pattern
1. 工厂方法模式可以解决简单工厂模式当中的OCP问题。
怎么解决的?一个工厂对应生产一种产品。
这样工厂就不是全能类了,不是上帝类了。
另外,也可以符合OCP原则。
2. 工厂方法模式中的角色:
* 抽象产品角色 Weapon
* 具体产品角色 Dagger Gun
* 抽象工厂角色 WeaponFactory
* 具体工厂角色 DaggerFactory GunFactory
3. 工厂方法模式的优点:
当你扩展一个产品的时候,符合OCP原则,因为只需要添加两个类,一个类是具体产品类,一个类是具体工厂类。都是添加类,没有修改之前的代码,所以符合OCP。
● 一个调用者想创建一个对象,只要知道其名称就可以了。
● 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
● 屏蔽产品的具体实现,调用者只关心产品的接口。
4. 工厂方法模式的缺点:
每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,
在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
6.4 抽象工厂模式(了解)
抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类。
抽象工厂模式特点:抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。它有多个抽象产品类,每个抽象产品类可以派生出多个具体产品类,一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。每一个模式都是针对一定问题的解决方案,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式针对的是多个产品等级结果。
抽象工厂中包含4个角色:
- 抽象工厂角色
- 具体工厂角色
- 抽象产品角色
- 具体产品角色
抽象工厂模式的类图如下:
抽象工厂模式代码如下:
第一部分:武器产品族
package com.powernode.product;
/**
* 武器产品族
* @author 动力节点
* @version 1.0
* @className Weapon
* @since 1.0
**/
public abstract class Weapon {
public abstract void attack();
}
package com.powernode.product;
/**
* 武器产品族中的产品等级1
* @author 动力节点
* @version 1.0
* @className Gun
* @since 1.0
**/
public class Gun extends Weapon{
@Override
public void attack() {
System.out.println("开枪射击!");
}
}
package com.powernode.product;
/**
* 武器产品族中的产品等级2
* @author 动力节点
* @version 1.0
* @className Dagger
* @since 1.0
**/
public class Dagger extends Weapon{
@Override
public void attack() {
System.out.println("砍丫的!");
}
}
第二部分:水果产品族
package com.powernode.product;
/**
* 水果产品族
* @author 动力节点
* @version 1.0
* @className Fruit
* @since 1.0
**/
public abstract class Fruit {
/**
* 所有果实都有一个成熟周期。
*/
public abstract void ripeCycle();
}
package com.powernode.product;
/**
* 水果产品族中的产品等级1
* @author 动力节点
* @version 1.0
* @className Orange
* @since 1.0
**/
public class Orange extends Fruit{
@Override
public void ripeCycle() {
System.out.println("橘子的成熟周期是10个月");
}
}
package com.powernode.product;
/**
* 水果产品族中的产品等级2
* @author 动力节点
* @version 1.0
* @className Apple
* @since 1.0
**/
public class Apple extends Fruit{
@Override
public void ripeCycle() {
System.out.println("苹果的成熟周期是8个月");
}
}
第三部分:抽象工厂类
package com.powernode.factory;
import com.powernode.product.Fruit;
import com.powernode.product.Weapon;
/**
* 抽象工厂
* @author 动力节点
* @version 1.0
* @className AbstractFactory
* @since 1.0
**/
public abstract class AbstractFactory {
public abstract Weapon getWeapon(String type);
public abstract Fruit getFruit(String type);
}
第四部分:具体工厂类
package com.powernode.factory;
import com.powernode.product.Dagger;
import com.powernode.product.Fruit;
import com.powernode.product.Gun;
import com.powernode.product.Weapon;
/**
* 武器族工厂
* @author 动力节点
* @version 1.0
* @className WeaponFactory
* @since 1.0
**/
public class WeaponFactory extends AbstractFactory{
public Weapon getWeapon(String type){
if (type == null || type.trim().length() == 0) {
return null;
}
if ("Gun".equals(type)) {
return new Gun();
} else if ("Dagger".equals(type)) {
return new Dagger();
} else {
throw new RuntimeException("无法生产该武器");
}
}
@Override
public Fruit getFruit(String type) {
return null;
}
}
package com.powernode.factory;
import com.powernode.product.*;
/**
* 水果族工厂
* @author 动力节点
* @version 1.0
* @className FruitFactory
* @since 1.0
**/
public class FruitFactory extends AbstractFactory{
@Override
public Weapon getWeapon(String type) {
return null;
}
public Fruit getFruit(String type){
if (type == null || type.trim().length() == 0) {
return null;
}
if ("Orange".equals(type)) {
return new Orange();
} else if ("Apple".equals(type)) {
return new Apple();
} else {
throw new RuntimeException("我家果园不产这种水果");
}
}
}
第五部分:客户端程序
package com.powernode.client;
import com.powernode.factory.AbstractFactory;
import com.powernode.factory.FruitFactory;
import com.powernode.factory.WeaponFactory;
import com.powernode.product.Fruit;
import com.powernode.product.Weapon;
/**
* @author 动力节点
* @version 1.0
* @className Client
* @since 1.0
**/
public class Client {
public static void main(String[] args) {
// 客户端调用方法时只面向AbstractFactory调用方法。
AbstractFactory factory = new WeaponFactory(); // 注意:这里的new WeaponFactory()可以采用 简单工厂模式 进行隐藏。
Weapon gun = factory.getWeapon("Gun");
Weapon dagger = factory.getWeapon("Dagger");
gun.attack();
dagger.attack();
AbstractFactory factory1 = new FruitFactory(); // 注意:这里的new FruitFactory()可以采用 简单工厂模式 进行隐藏。
Fruit orange = factory1.getFruit("Orange");
Fruit apple = factory1.getFruit("Apple");
orange.ripeCycle();
apple.ripeCycle();
}
}
执行结果:
抽象工厂模式的优缺点:
- 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
- 缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在AbstractFactory里加代码,又要在具体的里面加代码。
七、Bean的实例化方式
Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在Spring中为Bean对象的创建准备了多种方案,目的是:更加灵活)
- 第一种:通过构造方法实例化
- 第二种:通过简单工厂模式实例化(factory-method 属性指定工厂类当中的静态方法。也就是告诉Spring框架,调用这个方法可以获取Bean。)
- 第三种:通过factory-bean实例化
- 第四种:通过FactoryBean接口实例化
7.1 通过构造方法实例化
我们之前一直使用的就是这种方式。默认情况下,会调用Bean的无参数构造方法。
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User {
public User() {
System.out.println("User类的无参数构造方法执行。");
}
}
<?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">
<!--Spring提供的实例化方式,第一种:在spring配置文件中直接配置类全路径,Spring会自动调用该类的无参数构造方法来实例化Bean-->
<bean id="userBean" class="com.powernode.spring6.bean.User"/>
</beans>
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className SpringInstantiationTest
* @since 1.0
**/
public class SpringInstantiationTest {
@Test
public void testConstructor(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}
}
执行结果:
7.2 通过简单工厂模式实例化
第一步:定义一个Bean
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Vip
* @since 1.0
**/
public class Vip {
}
第二步:编写简单工厂模式当中的工厂类
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className VipFactory
* @since 1.0
**/
public class VipFactory {
// 工厂类中有一个静态方法。
// 简单工厂模式又叫做:静态工厂方法模式。
public static Vip get(){
// 这里最终实际上创建的时候还是我们负责new的对象。
return new Vip();
}
}
第三步:在Spring配置文件中指定创建该Bean的方法(使用factory-method属性指定)
你需要在Spring配置文件中告诉Spring框架,调用哪个类的哪个方法获取Bean
<!--Spring提供的实例化方式,第二种:通过简单工厂模式。你需要在Spring配置文件中告诉Spring框架,调用哪个类的哪个方法获取Bean-->
<!--factory-method 属性指定的是工厂类当中的静态方法。也就是告诉Spring框架,调用这个方法可以获取Bean。-->
<bean id="vipBean" class="com.powernode.spring6.bean.VipFactory" factory-method="get"/>
第四步:编写测试程序
@Test
public void testSimpleFactory(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Vip vip = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vip);
}
执行结果:
7.3 通过factory-bean实例化
这种方式本质上是:通过工厂方法模式进行实例化。
第一步:定义一个Bean
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Order
* @since 1.0
**/
public class Order {
}
第二步:定义具体工厂类,工厂类中定义实例方法
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className OrderFactory
* @since 1.0
**/
public class OrderFactory {
// 工厂方法模式中的具体工厂角色中的方法是:实例方法。
public Order get(){
// 实际上new这个对象还是我们程序员自己new的。
return new Order();
}
}
第三步:在Spring配置文件中指定factory-bean以及factory-method
告诉Spring框架,调用哪个对象的哪个方法来获取Bean。
<!--Spring提供的实例化方式,第三种:通过工厂方法模式。通过 factory-bean属性 + factory-method属性来共同完成。-->
<!--告诉Spring框架,调用哪个对象的哪个方法来获取Bean。-->
<bean id="orderFactory" class="com.powernode.spring6.bean.OrderFactory"/>
<!--以下的配置很关键,factory-bean属性告诉Spring调用哪个对象。factory-method告诉Spring调用该对象的哪个方法。-->
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>
第四步:编写测试程序
@Test
public void testSelfFactoryBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Order orderBean = applicationContext.getBean("orderBean", Order.class);
System.out.println(orderBean);
}
执行结果:
7.4 通过FactoryBean接口实例化
以上的第三种方式中,factory-bean是我们自定义的,factory-method也是我们自己定义的。
在Spring中,当你编写的类直接实现FactoryBean接口之后,factory-bean不需要指定了,factory-method也不需要指定了。
factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject()方法。
第一步:定义一个Bean
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Person
* @since 1.0
**/
public class Person {
}
第二步:编写一个类实现FactoryBean接口
package com.powernode.spring6.bean;
import org.springframework.beans.factory.FactoryBean;
/**
* @author 动力节点
* @version 1.0
* @className PersonFactoryBean
* @since 1.0
**/
public class PersonFactoryBean implements FactoryBean<Person> {
// PersonFactoryBean也是一个Bean。只不过这个Bean比较特殊。叫做工厂Bean。
// 通过工厂Bean这个特殊的Bean可以获取一个普通的Bean。
@Override
public Person getObject() throws Exception {
// 最终这个Bean的创建还是程序员自己new的。
return new Person();
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
// 这个方法在接口中有默认实现。
// 默认返回true,表示单例的。
// 如果想多例(原型),直接将这个方法修改为return false;即可。
return true;
}
}
第三步:在Spring配置文件中配置FactoryBean
<!--Spring提供的实例化方式,第四种:通过FactoryBean接口来实现。-->
<!--这种方式实际上就是第三种方式的简化。-->
<!--由于你编写的类实现了FactoryBean接口,所以这个类是一个特殊的类,不需要你再手动指定:factory-bean、factory-method-->
<!--通过一个特殊的Bean:工厂Bean。来返回一个普通的Bean Person对象。-->
<!--通过FactoryBean这个工厂Bean主要是想对普通Bean进行加工处理。-->
<bean id="personBean" class="com.powernode.spring6.bean.PersonFactoryBean"/>
测试程序:
@Test
public void testFactoryBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Person personBean = applicationContext.getBean("personBean", Person.class);
System.out.println(personBean);
Person personBean2 = applicationContext.getBean("personBean", Person.class);
System.out.println(personBean2);
}
执行结果:
FactoryBean在Spring中是一个接口。被称为“工厂Bean”。“工厂Bean”是一种特殊的Bean。所有的“工厂Bean”都是用来协助Spring框架来创建其他Bean对象的。
7.5 BeanFactory和FactoryBean的区别
7.5.1 BeanFactory
Spring IoC容器的顶级接口,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。
BeanFactory是工厂。
7.5.2 FactoryBean
FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。(加工)
在Spring中,Bean可以分为两类:
- 第一类:普通Bean
- 第二类:工厂Bean(记住:工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。(对普通Bean进行加工))
7.6 注入自定义Date
我们前面说过,java.util.Date在Spring中被当做简单类型,简单类型在注入的时候可以直接使用value属性或value标签来完成。但我们之前已经测试过了,对于Date类型来说,采用value属性或value标签赋值的时候,对日期字符串的格式要求非常严格,必须是这种格式的:Mon Oct 10 14:30:26 CST 2022。其他格式是不会被识别的。如以下代码:
package com.powernode.spring6.bean;
import java.util.Date;
/**
* @author 动力节点
* @version 1.0
* @className Student
* @since 1.0
**/
public class Student {
private Date birth;
public void setBirth(Date birth) {
this.birth = birth;
}
@Override
public String toString() {
return "Student{" +
"birth=" + birth +
'}';
}
}
<bean id="studentBean" class="com.powernode.spring6.bean.Student">
<property name="birth" value="Mon Oct 10 14:30:26 CST 2002"/>
</bean>
@Test
public void testDate(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Student studentBean = applicationContext.getBean("studentBean", Student.class);
System.out.println(studentBean);
}
执行结果:
如果把日期格式修改一下:
<bean id="studentBean" class="com.powernode.spring6.bean.Student">
<property name="birth" value="2002-10-10"/>
</bean>
执行结果:
这种情况下,我们就可以使用FactoryBean来完成这个骚操作。
编写DateFactoryBean实现FactoryBean接口:
package com.powernode.spring6.bean;
import org.springframework.beans.factory.FactoryBean;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author 动力节点
* @version 1.0
* @className DateFactoryBean
* @since 1.0
**/
public class DateFactoryBean implements FactoryBean<Date> {
// 定义属性接收日期字符串
private String date;
// 通过构造方法给日期字符串属性赋值
public DateFactoryBean(String date) {
this.date = date;
}
@Override
public Date getObject() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(this.date);
}
@Override
public Class<?> getObjectType() {
return null;
}
}
编写spring配置文件:
<!--通过工厂Bean:DateFactoryBean 来返回普通Bean:java.util.Date -->
<bean id="dateBean" class="com.powernode.spring6.bean.DateFactoryBean">
<constructor-arg name="date" value="1999-10-11"/>
</bean>
<bean id="studentBean" class="com.powernode.spring6.bean.Student">
<property name="birth" ref="dateBean"/>
</bean>
执行测试程序:
八、Bean的生命周期
8.1 什么是Bean的生命周期
Spring其实就是一个管理Bean对象的工厂。它负责对象的创建,对象的销毁等。
所谓的生命周期就是:对象从创建开始到最终销毁的整个过程。
什么时候创建Bean对象?
创建Bean对象的前后会调用什么方法?
Bean对象什么时候销毁?
Bean对象的销毁前后调用什么方法?
8.2 为什么要知道Bean的生命周期
其实生命周期的本质是:在哪个时间节点上调用了哪个类的哪个方法。
我们需要充分的了解在这个生命线上,都有哪些特殊的时间节点。
只有我们知道了特殊的时间节点都在哪,到时我们才可以确定代码写到哪。
我们可能需要在某个特殊的时间点上执行一段特定的代码,这段代码就可以放到这个节点上。当生命线走到这里的时候,自然会被调用。
8.3 Bean的生命周期之5步
Bean生命周期的管理,可以参考Spring的源码:AbstractAutowireCapableBeanFactory类的doCreateBean()方法。
Bean生命周期可以粗略的划分为五大步:
- 第一步:实例化Bean
- 第二步:Bean属性赋值
- 第三步:初始化Bean
- 第四步:使用Bean
- 第五步:销毁Bean
编写测试程序:
定义一个Bean
package com.powernode.spring6.bean;
/**
* Bean的生命周期按照粗略的五步的话:
* 第一步:实例化Bean(调用无参数构造方法。)
* 第二步:给Bean属性赋值(调用set方法。)
* 第三步:初始化Bean(会调用Bean的init方法。注意:这个init方法需要自己写,自己配。)
* 第四步:使用Bean
* 第五步:销毁Bean(会调用Bean的destroy方法。注意:这个destroy方法需要自己写,自己配。)
**/
public class User {
private String name;
public User() {
System.out.println("1.实例化Bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}
public void initBean(){
System.out.println("3.初始化Bean");
}
public void destroyBean(){
System.out.println("5.销毁Bean");
}
}
<?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">
<!--
init-method属性指定初始化方法。destroy-method属性指定销毁方法。
需要手动指定初始化方法,和销毁方法。
-->
<bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean">
<property name="name" value="zhangsan"/>
</bean>
</beans>
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className BeanLifecycleTest
* @since 1.0
**/
public class BeanLifecycleTest {
@Test
public void testLifecycle(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println("4.使用Bean");
// 只有正常关闭spring容器才会执行销毁方法。必须手动关闭Spring容器,这样Spring容器才会销毁Bean
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}
}
执行结果:
需要注意的:
- 第一:只有正常关闭spring容器,bean的销毁方法才会被调用。
- 第二:ClassPathXmlApplicationContext类才有close()方法。
- 第三:配置文件中的init-method指定初始化方法。destroy-method指定销毁方法。
8.4 Bean生命周期之7步
在以上的5步中,第3步是初始化Bean,如果你还想在初始化前和初始化后添加代码,可以加入“Bean后处理器”。
编写一个类实现BeanPostProcessor类,并且重写before和after方法:
package com.powernode.spring6.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* @author 动力节点
* @version 1.0
* @className LogBeanPostProcessor
* @since 1.0
**/
public class LogBeanPostProcessor implements BeanPostProcessor {
// 方法有两个参数:
// 第一个参数:刚创建的bean对象。
// 第二个参数:bean的名字。
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的before方法执行,即将开始初始化");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}
在spring.xml文件中配置“Bean后处理器”:
<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>
一定要注意:在spring.xml文件中配置的Bean后处理器将作用于当前配置文件中所有的Bean。
执行测试程序:
如果加上Bean后处理器的话,Bean的生命周期就是7步了:
8.5 Bean生命周期之10步
如果根据源码跟踪,可以划分更细粒度的步骤,10步:
上图中检查Bean是否实现了Aware的相关接口是什么意思?
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
- 当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
- 当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
- 当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
测试以上10步,可以让User类实现5个接口,并实现所有方法:
- BeanNameAware
- BeanClassLoaderAware
- BeanFactoryAware
- InitializingBean
- DisposableBean
代码如下:
package com.powernode.spring6.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
private String name;
public User() {
System.out.println("1.实例化Bean");
}
public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}
// 这个方法需要自己写,自己配。方法名随意。
public void initBean(){
System.out.println("6.初始化Bean");
}
// 这个方法需要自己写,自己配。方法名随意。
public void destroyBean(){
System.out.println("10.销毁Bean");
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("3.类加载器:" + classLoader);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("3.Bean工厂:" + beanFactory);
}
@Override
public void setBeanName(String name) {
System.out.println("3.bean名字:" + name);
}
@Override
public void destroy() throws Exception {
System.out.println("9.DisposableBean's destroy方法执行");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("5. InitializingBean's afterPropertiesSet执行");
}
}
package com.powernode.spring6.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* @author 动力节点
* @version 1.0
* @className LogBeanPostProcessor
* @since 1.0
**/
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("4.Bean后处理器的before方法执行,即将开始初始化");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("7.Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}
执行结果:
通过测试可以看出来:
- InitializingBean的方法早于init-method的执行。
- DisposableBean的方法早于destroy-method的执行。
对于SpringBean的生命周期,掌握之前的7步即可。够用。
Bean的生命周期五步:
第一步:实例化Bean
第二步:Bean属性赋值
第三步:初始化Bean
第四步:使用Bean
第五步:销毁Bean
Bean生命周期七步:比五步添加的那两步在哪里?在初始化Bean的前和后。
第一步:实例化Bean
第二步:Bean属性赋值
第三步:执行“Bean后处理器”的before方法。
第四步:初始化Bean
第五步:执行“Bean后处理器”的after方法。
第六步:使用Bean
第七步:销毁Bean
Bean生命周期十步:比七步添加的那三步在哪里?
点位1:在“Bean后处理器”before方法之前
干了什么事儿?
检查Bean是否实现了Aware相关的接口,如果实现了接口则调用这些接口中的方法。
然后调用这些方法的目的是为了给你传递一些数据,让你更加方便使用。
点位2:在“Bean后处理器”before方法之后
干了什么事儿?
检查Bean是否实现了InitializingBean接口,如果实现了,则调用接口中的方法。
点位3:使用Bean之后,或者说销毁Bean之前
干了什么事儿?
检查Bean是否实现了DisposableBean接口,如果实现了,则调用接口中的方法。
添加的这三个点位的特点:都是在检查你这个Bean是否实现了某些特定的接口,如果实现了这些接口,则Spring容器会调用这个接口中的方法。
8.6 Bean的作用域不同,管理方式不同
Spring 根据Bean的作用域来选择管理方式。
- 对于singleton作用域的Bean,Spring 能够精确地知道该Bean何时被创建,何时初始化完成,以及何时被销毁;
- 而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
我们把之前User类的spring.xml文件中的配置scope设置为prototype:
<?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">
<!--
init-method属性指定初始化方法。
destroy-method属性指定销毁方法。
-->
<bean id="userBean" class="com.powernode.spring6.bean.User" init-method="initBean" destroy-method="destroyBean" scope="prototype">
<property name="name" value="zhangsan"/>
</bean>
<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.powernode.spring6.bean.LogBeanPostProcessor"/>
</beans>
/**
* Spring容器只对singleton的Bean进行完整的生命周期管理。
* 如果是prototype作用域的Bean,Spring容器只负责将该Bean初始化完毕。等客户端程序一旦获取到该Bean之后,Spring容器就不再管理该对象的生命周期了。
*/
执行测试程序:
通过测试一目了然。只执行了前8步,第9和10都没有执行。
8.7 自己new的对象如何让Spring管理
有些时候可能会遇到这样的需求,某个java对象是我们自己new的,然后我们希望这个对象被Spring容器管理,怎么实现?
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User {
}
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
/**
* @author 动力节点
* @version 1.0
* @className RegisterBeanTest
* @since 1.0
**/
public class RegisterBeanTest {
@Test
public void testBeanRegister(){
// 自己new的对象
User user = new User();
System.out.println(user);
// 将以上自己new的这个对象纳入Spring容器来管理。半路上交给Spring来管理。
// 创建 默认可列表BeanFactory 对象
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 注册Bean
factory.registerSingleton("userBean", user);
// 从spring容器中获取bean
User userBean = factory.getBean("userBean", User.class);
System.out.println(userBean);
}
}
执行结果:
九、Bean的循环依赖问题
9.1 什么是Bean的循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Husband
* @since 1.0
**/
public class Husband {
private String name;
private Wife wife;
}
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Wife
* @since 1.0
**/
public class Wife {
private String name;
private Husband husband;
}
9.2 singleton下的set注入产生的循环依赖
我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Husband
* @since 1.0
**/
public class Husband {
private String name;
private Wife wife;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
// toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
package com.powernode.spring6.bean;
/**
* @author 动力节点
* @version 1.0
* @className Wife
* @since 1.0
**/
public class Wife {
private String name;
private Husband husband;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}
// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
<?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">
<!--singleton + setter模式下的循环依赖是没有任何问题的。-->
<!--singleton表示在整个Spring容器当中是单例的,独一无二的对象。-->
<!--
在singleton + setter模式下,为什么循环依赖不会出现问题,Spring是如何应对的?
主要的原因是,在这种模式下Spring对Bean的管理主要分为清晰的两个阶段:
第一个阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化之后,马上进行 “曝光”【不等属性赋值就曝光】
第二个阶段:Bean“曝光”之后,再进行属性的赋值(调用set方法。)。
核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成的。
注意:只有在scope是singleton的情况下,Bean才会采取提前“曝光”的措施。
-->
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="singleton">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>
</beans>
package com.powernode.spring6.test;
import com.powernode.spring6.bean.Husband;
import com.powernode.spring6.bean.Wife;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className CircularDependencyTest
* @since 1.0
**/
public class CircularDependencyTest {
@Test
public void testSingletonAndSet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
System.out.println(husbandBean);
System.out.println(wifeBean);
}
}
执行结果:
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
在singleton + setter模式下,为什么循环依赖不会出现问题,Spring是如何应对的?
主要的原因是,在这种模式下Spring对Bean的管理主要分为清晰的两个阶段:
第一个阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化之后,马上进行 “曝光”【不等属性赋值就曝光】
第二个阶段:Bean“曝光”之后,再进行属性的赋值(调用set方法。)。
核心解决方案是:实例化对象和对象的属性赋值分为两个阶段来完成的。
注意:只有在scope是singleton的情况下,Bean才会采取提前“曝光”的措施。
9.3 prototype下的set注入产生的循环依赖
我们再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?
<?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">
<!--在prototype + setter模式下的循环依赖,存在问题,会出现异常!-->
<!--BeanCurrentlyInCreationException 当前的Bean正在处于创建中异常。。。-->
<!-- 注意:当两个bean的scope都是prototype的时候,才会出现异常。如果其中任意一个是singleton的,就不会出现异常。-->
<bean id="husbandBean" class="com.powernode.spring6.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.powernode.spring6.bean.Wife" scope="prototype">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>
</beans>
执行测试程序:发生了异常,异常信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘husbandBean’: Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
… 44 more
翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?
通过测试得知,当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。
大家可以测试一下,以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。
为什么两个Bean都是prototype时会出错呢?
9.4 singleton下的构造注入产生的循环依赖
我们再来测试一下singleton + 构造注入的方式下,spring是否能够解决这种循环依赖。
package com.powernode.spring6.bean2;
/**
* @author 动力节点
* @version 1.0
* @className Husband
* @since 1.0
**/
public class Husband {
private String name;
private Wife wife;
public Husband(String name, Wife wife) {
this.name = name;
this.wife = wife;
}
// -----------------------分割线--------------------------------
public String getName() {
return name;
}
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife +
'}';
}
}
package com.powernode.spring6.bean2;
/**
* @author 动力节点
* @version 1.0
* @className Wife
* @since 1.0
**/
public class Wife {
private String name;
private Husband husband;
public Wife(String name, Husband husband) {
this.name = name;
this.husband = husband;
}
// -------------------------分割线--------------------------------
public String getName() {
return name;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband +
'}';
}
}
<?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="hBean" class="com.powernode.spring6.bean2.Husband" scope="singleton">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="wife" ref="wBean"/>
</bean>
<bean id="wBean" class="com.powernode.spring6.bean2.Wife" scope="singleton">
<constructor-arg name="name" value="小花"/>
<constructor-arg name="husband" ref="hBean"/>
</bean>
</beans>
@Test
public void testSingletonAndConstructor(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
Husband hBean = applicationContext.getBean("hBean", Husband.class);
Wife wBean = applicationContext.getBean("wBean", Wife.class);
System.out.println(hBean);
System.out.println(wBean);
}
执行结果:发生了异常,信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘hBean’: Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
… 56 more
和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的。
为什么呢?
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。
9.5 Spring解决循环依赖的机理
Spring为什么可以解决set + singleton模式下循环依赖?
根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。
给Bean属性赋值的时候:调用setter方法来完成。
两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。
也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。
那么在Spring框架底层源码级别上是如何实现的呢?请看:
在以上类中包含三个重要的属性:
**Cache of singleton objects: bean name to bean instance. **单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
**Cache of early singleton objects: bean name to bean instance. **早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
**Cache of singleton factories: bean name to ObjectFactory. **单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
这三个缓存其实本质上是三个Map集合。
我们再来看,在该类中有这样一个方法addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光。
再分析下面的源码:
从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。
总结:
Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
源码分析:
DefaultSingletonBeanRegistry类中有三个比较重要的缓存:
private final Map<String, Object> singletonObjects 一级缓存
private final Map<String, Object> earlySingletonObjects 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories 三级缓存
这三个缓存都是Map集合。
Map集合的key存储的都是bean的name(bean id)。
一级缓存存储的是:单例Bean对象。完整的单例Bean对象,也就是说这个缓存中的Bean对象的属性都已经赋值了。是一个完整的Bean对象。
二级缓存存储的是:早期的单例Bean对象。这个缓存中的单例Bean对象的属性没有赋值。只是一个早期的实例对象。
三级缓存存储的是:单例工厂对象。这个里面存储了大量的“工厂对象”,每一个单例Bean对象都会对应一个单例工厂对象。
这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。
十、回顾反射机制
10.1 分析方法四要素
我们先来看一下,不使用反射机制调用一个方法需要几个要素的参与。
有一个这样的类:
package com.powernode.reflect;
/**
* @author 动力节点
* @version 1.0
* @className SystemService
* @since 1.0
**/
public class SystemService {
public void logout(){
System.out.println("退出系统");
}
public boolean login(String username, String password){
if ("admin".equals(username) && "admin123".equals(password)) {
return true;
}
return false;
}
}
编写程序调用方法:
package com.powernode.reflect;
/**
* @author 动力节点
* @version 1.0
* @className ReflectTest01
* @since 1.0
**/
public class ReflectTest01 {
public static void main(String[] args) {
// 创建对象
SystemService systemService = new SystemService();
// 调用方法并接收方法的返回值
boolean success = systemService.login("admin", "admin123");
System.out.println(success ? "登录成功" : "登录失败");
}
}
执行结果:
通过以上第16行代码可以看出,调用一个方法,一般涉及到4个要素:
- 调用哪个对象的(systemService)
- 哪个方法(login)
- 传什么参数(“admin”, “admin123”)
- 返回什么值(success)
调用哪个对象的哪个方法,传什么参数,返回什么值。
即使是使用反射机制调用方法,也同样需要具备这四个要素。
10.2 获取Method
要使用反射机制调用一个方法,首先你要获取到这个方法。
在反射机制中Method实例代表的是一个方法。那么怎么获取Method实例呢?
有这样一个类:
package com.powernode.reflect;
/**
* @author 动力节点
* @version 1.0
* @className SystemService
* @since 1.0
**/
public class SystemService {
public void logout(){
System.out.println("退出系统");
}
public boolean login(String username, String password){
if ("admin".equals(username) && "admin123".equals(password)) {
return true;
}
return false;
}
public boolean login(String password){
if("110".equals(password)){
return true;
}
return false;
}
}
我们如何获取到 logout()、login(String,String)、login(String) 这三个方法呢?
要获取方法Method,首先你需要获取这个类Class。
Class clazz = Class.forName("com.powernode.reflect.SystemService");
当拿到Class之后,调用getDeclaredMethod()方法可以获取到方法。
假如你要获取这个方法:login(String username, String password)
Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);
假如你要获取到这个方法:login(String password)
Method loginMethod = clazz.getDeclaredMethod("login", String.class);
获取一个方法,需要告诉Java程序,你要获取的方法的名字是什么,这个方法上每个形参的类型是什么。这样Java程序才能给你拿到对应的方法。
这样的设计也非常合理,因为在同一个类当中,方法是支持重载的,也就是说方法名可以一样,但参数列表一定是不一样的,所以获取一个方法需要提供方法名以及每个形参的类型。
假设有这样一个方法:
public void setAge(int age){
this.age = age;
}
你要获取这个方法的话,代码应该这样写:
Method setAgeMethod = clazz.getDeclaredMethod("setAge", int.class);
其中setAge是方法名,int.class是形参的类型。
如果要获取上面的logout方法,代码应该这样写:
Method logoutMethod = clazz.getDeclaredMethod("logout");
因为这个方法形式参数的个数是0个。所以只需要提供方法名就行了。你学会了吗?
10.3 调用Method
要让一个方法调用的话,就关联到四要素了:
- 调用哪个对象的
- 哪个方法
- 传什么参数
- 返回什么值
package com.powernode.reflect;
/**
* @author 动力节点
* @version 1.0
* @className SystemService
* @since 1.0
**/
public class SystemService {
public void logout(){
System.out.println("退出系统");
}
public boolean login(String username, String password){
if ("admin".equals(username) && "admin123".equals(password)) {
return true;
}
return false;
}
public boolean login(String password){
if("110".equals(password)){
return true;
}
return false;
}
}
假如我们要调用的方法是:login(String, String)
第一步:创建对象(四要素之首:调用哪个对象的)
Class clazz = Class.forName("com.powernode.reflect.SystemService");
Object obj = clazz.newInstance();
第二步:获取方法login(String,String)(四要素之一:哪个方法)
Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);
第三步:调用方法
Object retValue = loginMethod.invoke(obj, "admin", "admin123");
解说四要素:
- 哪个对象:obj
- 哪个方法:loginMethod
- 传什么参数:“admin”, “admin123”
- 返回什么值:retValue
package com.powernode.reflect;
import java.lang.reflect.Method;
/**
* @author 动力节点
* @version 1.0
* @className ReflectTest02
* @since 1.0
**/
public class ReflectTest02 {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("com.powernode.reflect.SystemService");
Object obj = clazz.newInstance();
Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);
Object retValue = loginMethod.invoke(obj, "admin", "admin123");
System.out.println(retValue);
}
}
执行结果:
那如果调用既没有参数,又没有返回值的logout方法,应该怎么做?
package com.powernode.reflect;
import java.lang.reflect.Method;
/**
* @author 动力节点
* @version 1.0
* @className ReflectTest03
* @since 1.0
**/
public class ReflectTest03 {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("com.powernode.reflect.SystemService");
Object obj = clazz.newInstance();
Method logoutMethod = clazz.getDeclaredMethod("logout");
logoutMethod.invoke(obj);
}
}
执行结果:
10.4 假设你知道属性名
假设有这样一个类:
package com.powernode.reflect;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
你知道以下这几条信息:
- 类名是:com.powernode.reflect.User
- 该类中有String类型的name属性和int类型的age属性。
- 另外你也知道该类的设计符合javabean规范。(也就是说属性私有化,对外提供setter和getter方法)
你如何通过反射机制给User对象的name属性赋值zhangsan,给age属性赋值20岁。
package com.powernode.reflect;
import java.lang.reflect.Method;
/**
* @author 动力节点
* @version 1.0
* @className UserTest
* @since 1.0
**/
public class UserTest {
public static void main(String[] args) throws Exception {
/*
需求:
假设你现在已知以下信息:
1. 有这样一个类,类名叫做:com.powernode.reflect.User
2. 这个类符合javabean规范。属性私有化,对外提供公开的setter和getter方法。
3. 你还知道这个类当中有一个属性,属性的名字叫做 age
4. 并且你还知道age属性的类型是int类型。
请使用反射机制调用set方法,给User对象的age属性赋值。
*/
String className = "com.powernode.reflect.User";
String propertyName = "age";
// 通过反射机制调用setAge(int)方法
// 获取类
Class<?> clazz = Class.forName(className);
// 获取方法名
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
// 根据属性名获取属性类型
Field field = clazz.getDeclaredField(propertyName);
// 获取方法
Method setMethod = clazz.getDeclaredMethod(setMethodName, field.getType());
// 准备对象
Object obj = clazz.newInstance();
// 调用方法
setMethod.invoke(obj, 20);
System.out.println(obj);
}
}
执行结果:
给User的name属性赋值zhangsan,这个大家可以尝试自己完成哦!!!
十一、手写Spring框架
Spring IoC容器的实现原理:工厂模式 + 解析XML + 反射机制。
我们给自己的框架起名为:myspring(我的春天)
第一步:创建模块myspring
采用Maven方式新建Module:myspring
打包方式采用jar,并且引入dom4j和jaxen的依赖,因为要使用它解析XML文件,还有junit依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.myspringframework</groupId>
<artifactId>myspring</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!--dom4j是一个能够解析XML文件的java组件。(搭配jaxen一起使用)-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第二步:准备好我们要管理的Bean
准备好我们要管理的Bean(这些Bean在将来开发完框架之后是要删除的)
注意包名,不要用org.myspringframework包,因为这些Bean不是框架内置的。是将来使用我们框架的程序员提供的。
package com.powernode.myspring.bean;
/**
* @author 动力节点
* @version 1.0
* @className Address
* @since 1.0
**/
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
@Override
public String toString() {
return "Address{" +
"city='" + city + '\'' +
", street='" + street + '\'' +
", zipcode='" + zipcode + '\'' +
'}';
}
}
package com.powernode.myspring.bean;
/**
* @author 动力节点
* @version 1.0
* @className User
* @since 1.0
**/
public class User {
private String name;
private int age;
private Address addr;
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Address getAddr() {
return addr;
}
public void setAddr(Address addr) {
this.addr = addr;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", addr=" + addr +
'}';
}
}
第三步:准备myspring.xml配置文件
将来在框架开发完毕之后,这个文件也是要删除的。因为这个配置文件的提供者应该是使用这个框架的程序员。
文件名随意,我们这里叫做:myspring.xml
文件放在类路径当中即可,我们这里把文件放到类的根路径下。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userBean" class="com.powernode.myspring.bean.User">
<property name="name" value="张三"/>
<property name="age" value="20"/>
<property name="addr" ref="addrBean"/>
</bean>
<bean id="addrBean" class="com.powernode.myspring.bean.Address">
<property name="city" value="北京"/>
<property name="street" value="大兴区"/>
<property name="zipcode" value="1000001"/>
</bean>
</beans>
使用value给简单属性赋值。使用ref给非简单属性赋值。
第四步:编写ApplicationContext接口
ApplicationContext接口中提供一个getBean()方法,通过该方法可以获取Bean对象。
注意包名:这个接口就是myspring框架中的一员了。
package org.myspringframework.core;
/**
* @author 动力节点
* @version 1.0
* @className ApplicationContext
* @since 1.0
**/
public interface ApplicationContext {
/**
* 根据bean的id获取bean实例。
* @param beanId bean的id
* @return bean实例
*/
Object getBean(String beanId);
}
第五步:编写ClassPathXmlApplicationContext
ClassPathXmlApplicationContext是ApplicationContext接口的实现类。该类从类路径当中加载myspring.xml配置文件。
package org.myspringframework.core;
/**
* @author 动力节点
* @version 1.0
* @className ClassPathXmlApplicationContext
* @since 1.0
**/
public class ClassPathXmlApplicationContext implements ApplicationContext{
@Override
public Object getBean(String beanId) {
return null;
}
}
第六步:确定采用Map集合存储Bean
确定采用Map集合存储Bean实例。Map集合的key存储beanId,value存储Bean实例。Map<String,Object>
在ClassPathXmlApplicationContext类中添加Map<String,Object>属性。
并且在ClassPathXmlApplicationContext类中添加构造方法,该构造方法的参数接收myspring.xml文件。
同时实现getBean方法。
package org.myspringframework.core;
import java.util.HashMap;
import java.util.Map;
/**
* @author 动力节点
* @version 1.0
* @className ClassPathXmlApplicationContext
* @since 1.0
**/
public class ClassPathXmlApplicationContext implements ApplicationContext{
/**
* 存储bean的Map集合
*/
private Map<String,Object> beanMap = new HashMap<>();
/**
* 在该构造方法中,解析myspring.xml文件,创建所有的Bean实例,并将Bean实例存放到Map集合中。
* @param resource 配置文件路径(要求在类路径当中)
*/
public ClassPathXmlApplicationContext(String resource) {
}
@Override
public Object getBean(String beanId) {
return beanMap.get(beanId);
}
}
第七步:解析配置文件实例化所有Bean
在ClassPathXmlApplicationContext的构造方法中解析配置文件,获取所有bean的类名,通过反射机制调用无参数构造方法创建Bean。并且将Bean对象存放到Map集合中。
/**
* 在该构造方法中,解析myspring.xml文件,创建所有的Bean实例,并将Bean实例存放到Map集合中。
* @param resource 配置文件路径(要求在类路径当中)
*/
public ClassPathXmlApplicationContext(String resource) {
try {
// 解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObjects集合当中。
// 这是dom4j解析XML文件的核心对象。
SAXReader reader = new SAXReader();
// 获取一个输入流,指向配置文件
InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(resource);
// 读文件
Document document = reader.read(in);
// 获取所有的bean标签(XML语法 //可以获取所有同名标签)
List<Node> beanNodes = document.selectNodes("//bean");
// 遍历bean标签
beanNodes.forEach(beanNode -> {
// 向下转型的目的是为了使用Element接口里更加丰富的方法。
Element beanElt = (Element) beanNode;
// 获取id
String id = beanElt.attributeValue("id");
// 获取className
String className = beanElt.attributeValue("class");
try {
// 通过反射机制创建对象,将其放到Map集合中,提前曝光。
// 获取Class
Class<?> clazz = Class.forName(className);
// 获取无参数构造方法
Constructor<?> defaultConstructor = clazz.getDeclaredConstructor();
// 调用无参数构造方法实例化Bean
Object bean = defaultConstructor.newInstance();
// 将Bean曝光,加入Map集合
beanMap.put(id, bean);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
第八步:测试能否获取到Bean
编写测试程序。
package com.powernode.myspring.test;
import org.junit.Test;
import org.myspringframework.core.ApplicationContext;
import org.myspringframework.core.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className MySpringTest
* @since 1.0
**/
public class MySpringTest {
@Test
public void testMySpring(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
Object userBean = applicationContext.getBean("userBean");
Object addrBean = applicationContext.getBean("addrBean");
System.out.println(userBean);
System.out.println(addrBean);
}
}
执行结果:
通过测试Bean已经实例化成功了,属性的值是null,这是我们能够想到的,毕竟我们调用的是无参数构造方法,所以属性都是默认值。
下一步就是我们应该如何给Bean的属性赋值呢?
第九步:给Bean的属性赋值
通过反射机制调用set方法,给Bean的属性赋值。
继续在ClassPathXmlApplicationContext构造方法中编写代码。
package org.myspringframework.core;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 动力节点
* @version 1.0
* @className ClassPathXmlApplicationContext
* @since 1.0
**/
public class ClassPathXmlApplicationContext implements ApplicationContext{
/**
* 存储bean的Map集合
*/
private Map<String,Object> beanMap = new HashMap<>();
/**
* 在该构造方法中,解析myspring.xml文件,创建所有的Bean实例,并将Bean实例存放到Map集合中。
* @param resource 配置文件路径(要求在类路径当中)
*/
public ClassPathXmlApplicationContext(String resource) {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(ClassLoader.getSystemClassLoader().getResourceAsStream(resource));
// 获取所有的bean标签
List<Node> beanNodes = document.selectNodes("//bean");
// 遍历集合(这里的遍历只实例化Bean,不给属性赋值。为什么要这样做?)
beanNodes.forEach(beanNode -> {
Element beanElt = (Element) beanNode;
// 获取id
String id = beanElt.attributeValue("id");
// 获取className
String className = beanElt.attributeValue("class");
try {
// 通过反射机制创建对象
Class<?> clazz = Class.forName(className);
Constructor<?> defaultConstructor = clazz.getDeclaredConstructor();
Object bean = defaultConstructor.newInstance();
// 存储到Map集合
beanMap.put(id, bean);
} catch (Exception e) {
e.printStackTrace();
}
});
// 再重新遍历集合,这次遍历是为了给Bean的所有属性赋值。
// 思考:为什么不在上面的循环中给Bean的属性赋值,而在这里再重新遍历一次呢?
// 通过这里你是否能够想到Spring是如何解决循环依赖的:实例化和属性赋值分开。
beanNodes.forEach(beanNode -> {
Element beanElt = (Element) beanNode;
// 获取bean的id
String beanId = beanElt.attributeValue("id");
// 获取该bean标签下所有的属性property标签
List<Element> propertyElts = beanElt.elements("property");
// 遍历所有属性标签
propertyElts.forEach(propertyElt -> {
try {
// 获取属性名
String propertyName = propertyElt.attributeValue("name");
// 获取属性类型
Class<?> propertyType = beanMap.get(beanId).getClass().getDeclaredField(propertyName).getType();
// 获取set方法名
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
// 获取set方法
Method setMethod = beanMap.get(beanId).getClass().getDeclaredMethod(setMethodName, propertyType);
// 获取属性的值,值可能是value,也可能是ref。
// 获取value
String propertyValue = propertyElt.attributeValue("value");
// 获取ref
String propertyRef = propertyElt.attributeValue("ref");
Object propertyVal = null;
if (propertyValue != null) {
// 说明这个值是【简单类型属性】
// 我们myspring框架声明一下:我们只支持这些类型为简单类型
// byte short int long float double boolean char
// Byte Short Integer Long Float Double Boolean Character
// String
// getSimpleName(),获取属性类型名(简单名,去掉包名)
// propertyType.getName(); "java.lang.String",带包名
String propertyTypeSimpleName = propertyType.getSimpleName();
switch (propertyTypeSimpleName) {
case "byte":
propertyVal = Byte.parseByte(propertyValue);
break;
case "short":
propertyVal = Short.parseShort(propertyValue);
break;
case "int":
propertyVal = Integer.parseInt(propertyValue);
break;
case "long":
propertyVal = Long.parseLong(propertyValue);
break;
case "float":
propertyVal = Float.parseFloat(propertyValue);
break;
case "double":
propertyVal = Double.parseDouble(propertyValue);
break;
case "boolean":
propertyVal = Boolean.parseBoolean(propertyValue);
break;
case "char":
propertyVal = propertyValue.charAt(0);
break;
case "Byte":
propertyVal = Byte.valueOf(propertyValue);
break;
case "Short":
propertyVal = Short.valueOf(propertyValue);
break;
case "Integer":
propertyVal = Integer.valueOf(propertyValue);
break;
case "Long":
propertyVal = Long.valueOf(propertyValue);
break;
case "Float":
propertyVal = Float.valueOf(propertyValue);
break;
case "Double":
propertyVal = Double.valueOf(propertyValue);
break;
case "Boolean":
propertyVal = Boolean.valueOf(propertyValue);
break;
case "Character":
propertyVal = Character.valueOf(propertyValue.charAt(0));
break;
case "String":
propertyVal = propertyValue;
}
// 调用set方法(set方法没有返回值)
setMethod.invoke(beanMap.get(beanId), propertyVal);
}
if (propertyRef != null) {
// 说明这个值是【非简单类型属性】
// 调用set方法(set方法没有返回值)
setMethod.invoke(beanMap.get(beanId), beanMap.get(propertyRef));
}
} catch (Exception e) {
e.printStackTrace();
}
});
});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String beanId) {
return beanMap.get(beanId);
}
}
重点处理:当property标签中是value怎么办?是ref怎么办?
执行测试程序:
第十步:打包发布
将多余的类以及配置文件删除,使用maven打包发布。
第十一步:站在程序员角度使用myspring框架
新建模块:myspring-test
引入myspring框架的依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>myspring-test</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!--用myspring框架,需要引入依赖-->
<dependency>
<groupId>org.myspringframework</groupId>
<artifactId>myspring</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
编写Bean
package com.powernode.myspring.bean;
/**
* @author 动力节点
* @version 1.0
* @className UserDao
* @since 1.0
**/
public class UserDao {
public void insert(){
System.out.println("UserDao正在插入数据");
}
}
package com.powernode.myspring.bean;
/**
* @author 动力节点
* @version 1.0
* @className UserService
* @since 1.0
**/
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
System.out.println("UserService开始执行save操作");
userDao.insert();
System.out.println("UserService执行save操作结束");
}
}
编写myspring.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userServiceBean" class="com.powernode.myspring.bean.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.myspring.bean.UserDao"/>
</beans>
编写测试程序
package com.powernode.myspring.test;
import com.powernode.myspring.bean.UserService;
import org.junit.Test;
import org.myspringframework.core.ApplicationContext;
import org.myspringframework.core.ClassPathXmlApplicationContext;
/**
* @author 动力节点
* @version 1.0
* @className MySpringTest
* @since 1.0
**/
public class MySpringTest {
@Test
public void testMySpring(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
UserService userServiceBean = (UserService) applicationContext.getBean("userServiceBean");
userServiceBean.save();
}
}
执行结果
十二、Spring IoC注解式开发
12.1 回顾注解
注解的存在主要是为了简化XML的配置。Spring6倡导全注解开发。
我们来回顾一下:
- 第一:注解怎么定义,注解中的属性怎么定义?
- 第二:注解怎么使用?
- 第三:通过反射机制怎么读取注解?
注解怎么定义,注解中的属性怎么定义?
package com.powernode.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解。
* @version 1.0
* @className Component
* @since 1.0
**/
// 标注注解的注解,叫做元注解。@Target注解用来修饰@Component可以出现的位置。
// 以下表示@Component注解可以出现在类上、属性上。
//@Target(value = {ElementType.TYPE, ElementType.FIELD})
// 以下表示@Component注解可以出现在类上
//@Target(value = {ElementType.TYPE})
// 使用某个注解的时候,如果注解的属性名是value的话,value可以省略。
//@Target({ElementType.TYPE})
// 使用某个注解的时候,如果注解的属性值是数组,并且数组中只有一个元素,大括号可以省略。
@Target(ElementType.TYPE)
// @Retention(RetentionPolicy.SOURCE):表示该注解只被保留在java源文件中。
// @Retention(RetentionPolicy.CLASS):表示该注解被保存在class文件中。
// @Retention(RetentionPolicy.RUNTIME):表示该注解被保存在class文件中,并且可以被反射机制所读取。
// RetentionPolicy,保持性策略
// @Retention 也是一个元注解。用来标注@Component注解最终保留在class文件当中,并且可以被反射机制读取。
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
// 定义注解的属性
// String是属性类型
// value是属性名
String value();
// 其他的属性
// 属性类型String
// 属性名是name
//String name();
// 数组属性
// 属性类型是:String[]
// 属性名:names
//String[] names();
//int[] ages();
//int age();
}
以上是自定义了一个注解:Component
该注解上面修饰的注解包括:Target注解和Retention注解,这两个注解被称为元注解。
Target注解用来设置Component注解可以出现的位置,以上代表表示Component注解只能用在类和接口上。
Retention注解用来设置Component注解的保持性策略,以上代表Component注解可以被反射机制读取。
String value(); 是Component注解中的一个属性。该属性类型String,属性名是value。
注解怎么使用?
package com.powernode.bean;
import com.powernode.annotation.Component;
//@Component(属性名 = 属性值, 属性名 = 属性值, 属性名 = 属性值....)
@Component(value = "userBean")
public class User {
}
用法简单,语法格式:@注解类型名(属性名=属性值, 属性名=属性值, 属性名=属性值…)
userBean为什么使用双引号括起来,因为value属性是String类型,字符串。
另外如果属性名是value,则在使用的时候可以省略属性名,例如:
package com.powernode.bean;
import com.powernode.annotation.Component;
//@Component(value = "userBean")
@Component("userBean")
public class User {
}
通过反射机制怎么读取注解?
package com.powernode.client;
import com.powernode.annotation.Component;
/**
* @author 动力节点
* @version 1.0
* @className ReflectAnnotationTest1
* @since 1.0
**/
public class ReflectAnnotationTest1 {
public static void main(String[] args) throws Exception{
// 通过反射机制怎么读取注解
// 获取类
Class<?> aClass = Class.forName("com.powernode.bean.User");
// 判断类上面有没有这个注解
if (aClass.isAnnotationPresent(Component.class)) {
// 获取类上的注解
Component annotation = aClass.getAnnotation(Component.class);
// 访问注解属性
System.out.println(annotation.value());
}
}
}
接下来,我们来写一段程序,当Bean类上有Component注解时,则实例化Bean对象,如果没有,则不实例化对象。
我们准备两个Bean,一个上面有注解,一个上面没有注解。
package com.powernode.bean;
import com.powernode.annotation.Component;
@Component("userBean")
public class User {
}
package com.powernode.bean;
public class Vip {
}
假设我们现在只知道包名:com.powernode.bean。至于这个包下有多少个Bean我们不知道。哪些Bean上有注解,哪些Bean上没有注解,这些我们都不知道,如何通过程序全自动化判断。
package com.powernode.test;
import com.powernode.annotation.Component;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @author 动力节点
* @version 1.0
* @className Test
* @since 1.0
**/
public class Test {
public static void main(String[] args) throws Exception {
// 存放Bean的Map集合。key存储beanId。value存储Bean。
Map<String,Object> beanMap = new HashMap<>();
String packageName = "com.powernode.bean";
String path = packageName.replaceAll("\\.", "/");
URL url = ClassLoader.getSystemClassLoader().getResource(path);
File file = new File(url.getPath());
File[] files = file.listFiles();
Arrays.stream(files).forEach(f -> {
String className = packageName + "." + f.getName().split("\\.")[0];
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(Component.class)) {
Component component = clazz.getAnnotation(Component.class);
String beanId = component.value();
Object bean = clazz.newInstance();
beanMap.put(beanId, bean);
}
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println(beanMap);
}
}
执行结果:
通过反射机制怎么读取注解?
package com.powernode.client;
import com.powernode.annotation.Component;
/**
* @author 动力节点
* @version 1.0
* @className ReflectAnnotationTest1
* @since 1.0
**/
public class ReflectAnnotationTest1 {
public static void main(String[] args) throws Exception{
// 通过反射机制怎么读取注解
// 获取类
Class<?> aClass = Class.forName("com.powernode.bean.User");
// 判断类上面有没有这个注解
if (aClass.isAnnotationPresent(Component.class)) {
// 获取类上的注解
Component annotation = aClass.getAnnotation(Component.class);
// 访问注解属性
System.out.println(annotation.value());
}
}
}
目前只知道一个包的名字,扫描这个包下所有的类,当这个类上有@Component注解的时候,实例化该对象,然后放到Map集合中。
package com.powernode.client;
import com.powernode.annotation.Component;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class ComponentScan {
public static void main(String[] args){
Map<String,Object> beanMap = new HashMap<>();
// 目前只知道一个包的名字,扫描这个包下所有的类,当这个类上有@Component注解的时候,实例化该对象,然后放到Map集合中。
String packageName = "com.powernode.bean";
// 开始写扫描程序。
// . 这个正则表达式代表任意字符。这里的"."必须是一个普通的"."字符。不能是正则表达式中的"."
// 在正则表达式当中怎么表示一个普通的"."字符呢?使用 \. 正则表达式代表一个普通的 . 字符。
String packagePath = packageName.replaceAll("\\.", "/");
//System.out.println(packagePath);
// com是在类的根路径下的一个目录。
URL url = ClassLoader.getSystemClassLoader().getResource(packagePath);
String path = url.getPath();
//System.out.println(path);
// 获取一个绝对路径下的所有文件
File file = new File(path);
File[] files = file.listFiles();
Arrays.stream(files).forEach(f -> {
try {
//System.out.println(f.getName()); // Order.class User.class Vip.class
//System.out.println(f.getName().split("\\.")[0]); // Order User Vip
String className = packageName + "." + f.getName().split("\\.")[0];
//com.powernode.bean.Order com.powernode.bean.User com.powernode.bean.Vip
//System.out.println(className);
// 通过反射机制解析注解
Class<?> aClass = Class.forName(className);
// 判断类上是否有这个注解
if (aClass.isAnnotationPresent(Component.class)) {
// 获取注解
Component annotation = aClass.getAnnotation(Component.class);
String id = annotation.value();
// 有这个注解的都要创建对象
Object obj = aClass.newInstance();
beanMap.put(id, obj);
}
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println(beanMap);
}
}
12.2 声明Bean的注解
负责声明Bean的注解,常见的包括四个:
- @Component
- @Controller
- @Service
- @Repository
源码如下:
package com.powernode.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
package org.springframework.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
package org.springframework.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
package org.springframework.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
通过源码可以看到,@Controller、@Service、@Repository这三个注解都是@Component注解的别名。
也就是说:这四个注解的功能都一样。用哪个都可以。
只是为了增强程序的可读性,建议:
- 控制器类上使用:Controller
- service类上使用:Service
- dao类上使用:Repository
他们都是只有一个value属性。value属性用来指定bean的id,也就是bean的名字。
12.3 Spring注解的使用
如何使用以上的注解呢?
- 第一步:加入aop的依赖
- 第二步:在配置文件中添加context命名空间
- 第三步:在配置文件中指定扫描的包
- 第四步:在Bean类上使用注解
第一步:加入aop的依赖
我们可以看到当加入spring-context依赖之后,会关联加入aop的依赖。所以这一步不用做。
第二步:在配置文件中添加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">
</beans>
第三步:在配置文件中指定要扫描的包
<?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">
<!--给Spring框架指定要扫描哪些包中的类-->
<context:component-scan base-package="com.powernode.spring6.bean"/>
</beans>
第四步:在Bean类上使用注解
package com.powernode.spring6.bean;
import org.springframework.stereotype.Component;
@Component(value = "userBean")
public class User {
}
编写测试程序:
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AnnotationTest {
@Test
public void testBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println(userBean);
}
}
执行结果:
如果注解的属性名是value,那么value是可以省略的。
package com.powernode.spring6.bean;
import org.springframework.stereotype.Component;
@Component("vipBean")
public class Vip {
}
package com.powernode.spring6.test;
import com.powernode.spring6.bean.Vip;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AnnotationTest {
@Test
public void testBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vipBean);
}
}
执行结果:
如果把value属性彻底去掉,spring会被Bean自动取名吗?会的。并且默认名字的规律是:Bean类名首字母小写即可。
package com.powernode.spring6.bean;
import org.springframework.stereotype.Component;
@Component
public class BankDao {
/*
以上的这个注解 @Component就相当于以下的这个配置信息:
<bean id="order" class="com.powernode.spring6.bean.BankDao"></bean>
*/
}
也就是说,这个BankDao的bean的名字为:bankDao
测试一下
package com.powernode.spring6.test;
import com.powernode.spring6.bean.BankDao;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AnnotationTest {
@Test
public void testBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
BankDao bankDao = applicationContext.getBean("bankDao", BankDao.class);
System.out.println(bankDao);
}
}
执行结果:
我们将Component注解换成其它三个注解,看看是否可以用:
package com.powernode.spring6.bean;
import org.springframework.stereotype.Controller;
@Controller
public class BankDao {
}
执行结果:
剩下的两个注解大家可以测试一下。
如果是多个包怎么办?有两种解决方案:
- 第一种:在配置文件中指定多个包,用逗号隔开。
- 第二种:指定多个包的共同父包。
先来测试一下逗号(英文)的方式:
创建一个新的包:bean2,定义一个Bean类。
package com.powernode.spring6.bean2;
import org.springframework.stereotype.Service;
@Service
public class Order {
}
配置文件修改:
<?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.powernode.spring6.bean,com.powernode.spring6.bean2"/>
</beans>
测试程序:
package com.powernode.spring6.test;
import com.powernode.spring6.bean.BankDao;
import com.powernode.spring6.bean2.Order;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AnnotationTest {
@Test
public void testBean(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
BankDao bankDao = applicationContext.getBean("bankDao", BankDao.class);
System.out.println(bankDao);
Order order = applicationContext.getBean("order", Order.class);
System.out.println(order);
}
}
执行结果:
我们再来看看,指定共同的父包行不行:
<?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.powernode.spring6"/>
</beans>
执行测试程序:
12.4 选择性实例化Bean
假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与Bean管理,其他的都不实例化。这应该怎么办呢?
package com.powernode.spring6.bean3;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@Component
public class A {
public A() {
System.out.println("A的无参数构造方法执行");
}
}
@Controller
class B {
public B() {
System.out.println("B的无参数构造方法执行");
}
}
@Service
class C {
public C() {
System.out.println("C的无参数构造方法执行");
}
}
@Repository
class D {
public D() {
System.out.println("D的无参数构造方法执行");
}
}
@Controller
class E {
public E() {
System.out.println("E的无参数构造方法执行");
}
}
@Controller
class F {
public F() {
System.out.println("F的无参数构造方法执行");
}
}
我只想实例化bean3包下的Controller。配置文件这样写:
<?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">
<!--
第一种解决方案:
use-default-filters="false"
如果这个属性是false,表示com.powernode.spring6.bean3包下所有的带有声明Bean的注解全部失效。@Component @Controller @Service @Repository全部失效。
-->
<context:component-scan base-package="com.powernode.spring6.bean3" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
use-default-filters=“true” 表示:使用spring默认的规则,只要有Component、Controller、Service、Repository中的任意一个注解标注,则进行实例化。
use-default-filters=“false” 表示:不再spring默认实例化规则,即使有Component、Controller、Service、Repository这些注解标注,也不再实例化。
<context:include-filter type=“annotation” expression=“org.springframework.stereotype.Controller”/> 表示只有Controller进行实例化。
@Test
public void testChoose(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-choose.xml");
}
执行结果:
也可以将use-default-filters设置为true(不写就是true),并且采用exclude-filter方式排出哪些注解标注的Bean不参与实例化:
<!--
第二种解决方案:
use-default-filters="true"
如果这个属性的值是true,表示com.powernode.spring6.bean3下的所有的带有声明Bean的注解全部生效。
use-default-filters="true" 默认值就是true,不用写。
-->
<context:component-scan base-package="com.powernode.spring6.bean3">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
执行测试程序:
12.5 负责注入的注解
@Component @Controller @Service @Repository 这四个注解是用来声明Bean的,声明后这些Bean将被实例化。接下来我们看一下,如何给Bean的属性赋值。给Bean属性赋值需要用到这些注解:
- @Value
- @Autowired
- @Qualifier
- @Resource
12.5.1 @Value
当属性的类型是简单类型时,可以使用@Value注解进行注入。
package com.powernode.spring6.bean4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
@Value(value = "zhangsan")
private String name;
@Value("20")
private int age;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
开启包扫描:
<?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.powernode.spring6.bean4"/>
</beans>
@Test
public void testValue(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");
Object user = applicationContext.getBean("user");
System.out.println(user);
}
执行结果:
通过以上代码可以发现,我们并没有给属性提供setter方法,但仍然可以完成属性赋值。
如果提供setter方法,并且在setter方法上添加@Value注解,可以完成注入吗?尝试一下:
package com.powernode.spring6.bean4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
private String name;
private int age;
@Value("李四")
public void setName(String name) {
this.name = name;
}
@Value("30")
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
执行结果:
通过测试可以得知,@Value注解可以直接使用在属性上,也可以使用在setter方法上。都是可以的。都可以完成属性的赋值。
为了简化代码,以后我们一般不提供setter方法,直接在属性上使用@Value注解完成属性赋值。
出于好奇,我们再来测试一下,是否能够通过构造方法完成注入:
package com.powernode.spring6.bean4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
private String name;
private int age;
public User(@Value("隔壁老王") String name, @Value("33") int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
执行结果:
通过测试得知:@Value注解可以出现在属性上、setter方法上、以及构造方法的形参上。可见Spring给我们提供了多样化的注入。太灵活了。
12.5.2 @Autowired与@Qualifier
@Autowired注解可以用来注入非简单类型。被翻译为:自动连线的,或者自动装配。
单独使用@Autowired注解,默认根据类型装配。【默认是byType】
看一下它的源码:
package org.springframework.beans.factory.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
源码中有两处需要注意:
- 第一处:该注解可以标注在哪里?
- 构造方法上
- 方法上
- 形参上
- 属性上
- 注解上
- 第二处:该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注入的Bean存在或者不存在都没关系,存在的话就注入,不存在的话,也不报错。
我们先在属性上使用@Autowired注解:
package com.powernode.spring6.dao;
public interface UserDao {
void insert();
}
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository //纳入bean管理
public class UserDaoForMySQL implements UserDao{
@Override
public void insert() {
System.out.println("正在向mysql数据库插入User数据");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 纳入bean管理
public class UserService {
// @Autowired注解使用的时候,不需要指定任何属性,直接使用这个注解即可。
// 这个注解的作用是根据类型byType进行自动装配。
@Autowired // 在属性上注入
private UserDao userDao;
// 没有提供构造方法和setter方法。
public void save(){
userDao.insert();
}
}
<?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.powernode.spring6.dao,com.powernode.spring6.service"/>
</beans>
@Test
public void testAutowired(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-injection.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}
执行结果:
以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。
接下来,再来测试一下@Autowired注解出现在setter方法上:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
我们再来看看能不能出现在构造方法上:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
再来看看,这个注解能不能只标注在构造方法的形参上:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
public UserService(@Autowired UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
还有更劲爆的,当有参数的构造方法只有一个时,@Autowired注解可以省略。
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
// 如果一个类当中构造方法只有一个,并且构造方法上的参数和属性能够对应上。@Autowired注解可以省略。
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
当然,如果有多个构造方法,@Autowired肯定是不能省略的。
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public UserService(){
}
public void save(){
userDao.insert();
}
}
执行结果:
到此为止,我们已经清楚@Autowired注解可以出现在哪些位置了。
@Autowired注解默认是byType进行注入的,也就是说根据类型注入的,如果以上程序中,UserDao接口还有另外一个实现类,会出现问题吗?
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository //纳入bean管理
public class UserDaoForOracle implements UserDao{
@Override
public void insert() {
System.out.println("正在向Oracle数据库插入User数据");
}
}
当你写完这个新的实现类之后,此时IDEA工具已经提示错误信息了:
错误信息中说:不能装配,UserDao这个Bean的数量大于1.
怎么解决这个问题呢?当然要byName,根据名称进行装配了。
@Autowired注解和@Qualifier注解联合起来才可以根据名称进行装配,在@Qualifier注解中指定Bean名称。
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository // 这里没有给bean起名,默认名字是:userDaoForOracle
public class UserDaoForOracle implements UserDao{
@Override
public void insert() {
System.out.println("正在向Oracle数据库插入User数据");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
// @Autowired和@Qualifier联合使用,可以根据名字进行装配。
@Autowired
@Qualifier("userDaoForOracle") // 这个是bean的名字。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
总结:
- @Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
- 当带参数的构造方法只有一个,@Autowired注解可以省略。
- @Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
12.5.3 @Resource
@Resource注解也可以完成非简单类型注入。那它和@Autowired注解有什么区别?
- @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需要引入以下依赖。】
如果你是Spring6+版本请使用这个依赖
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
一定要注意:如果你用Spring6,要知道Spring6不再支持JavaEE,它支持的是JakartaEE9。(Oracle把JavaEE贡献给Apache了,Apache把JavaEE的名字改成JakartaEE了,大家之前所接触的所有的 javax. 包名统一修改为 jakarta.包名了。)
如果你是spring5-版本请使用这个依赖
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@Resource注解的源码如下:
测试一下:
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository("xyz")
public class UserDaoForOracle implements UserDao{
@Override
public void insert() {
System.out.println("正在向Oracle数据库插入User数据");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Resource(name = "xyz")
private UserDao userDao;
public void save(){
userDao.insert();
}
}
执行测试程序:
我们把UserDaoForOracle的名字xyz修改为userDao,让这个Bean的名字和UserService类中的UserDao属性名一致:
package com.powernode.spring6.dao;
import org.springframework.stereotype.Repository;
@Repository("userDao")
public class UserDaoForOracle implements UserDao{
@Override
public void insert() {
System.out.println("正在向Oracle数据库插入User数据");
}
}
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Resource
private UserDao userDao;
public void save(){
userDao.insert();
}
}
执行测试程序:
通过测试得知,当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
接下来把UserService类中的属性名修改一下:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Resource
private UserDao userDao2;
public void save(){
userDao2.insert();
}
}
执行结果:
根据异常信息得知:显然当通过name找不到的时候,自然会启动byType进行注入。以上的错误是因为UserDao接口下有两个实现类导致的。所以根据类型注入就会报错。
我们再来看@Resource注解使用在setter方法上可以吗?
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Resource
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
注意这个setter方法的方法名,setUserDao去掉set之后,将首字母变小写userDao,userDao就是name
执行结果:
当然,也可以指定name:
package com.powernode.spring6.service;
import com.powernode.spring6.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserDao userDao;
@Resource(name = "userDaoForMySQL")
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
执行结果:
一句话总结@Resource注解:默认byName注入,没有指定name时把属性名当做name,根据name找不到时,才会byType注入。byType注入时,某种类型的Bean只能有一个。
12.6 全注解式开发
所谓的全注解开发就是不再使用spring配置文件了。写一个配置类来代替配置文件。
package com.powernode.spring6.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;
/**
* 编写一个类,代替Spring框架的配置文件。
**/
@Configuration
@ComponentScan({"com.powernode.spring6.dao", "com.powernode.spring6.service"})
public class Spring6Configuration {
}
编写测试程序:不再new ClassPathXmlApplicationContext()对象了。
@Test
public void testNoXml(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}
执行结果: