spring之Bean的循环依赖问题、反射机制手写Spring框架、Spring IoC注解式开发
一、Bean的循环依赖问题
1.什么是Bean的循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
public class Husband {
private String name;
private Wife wife;
}
public class Wife {
private String name;
private Husband husband;
}
2.singleton下的set注入产生的循环依赖
我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?
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() +
'}';
}
}
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.spring6.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.spring6.bean.Wife" scope="singleton">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>
</beans>
Test
@Test
public void testCD(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
System.out.println(husbandBean);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
System.out.println(wifeBean);
}
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
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.spring6.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.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时会出错呢?
在Spring框架中,当两个prototype
作用域的Bean通过setter注入产生循环依赖时,会抛出BeanCurrentlyInCreationException
异常。这与prototype
作用域的特性及Spring的依赖解决机制有关,以下是具体原因:
⑴. prototype
作用域的核心特点
-
无缓存:
prototype
作用域的Bean每次请求时都会创建一个全新的实例,Spring不会缓存这些实例。 -
生命周期不托管:与单例Bean不同,
prototype
Bean的初始化和销毁方法由调用者管理,Spring容器不负责其完整生命周期。
⑵.循环依赖的解决机制对比
-
单例Bean的解决方案:
-
Spring通过三级缓存(
singletonFactories
、earlySingletonObjects
、singletonObjects
)提前暴露未完全初始化的Bean引用。 -
例如,Bean A和Bean B都是单例,Spring会先实例化A,在填充属性时发现需要B,然后实例化B并注入A的早期引用(未完成初始化的对象),最终完成两者的初始化。
-
-
prototype
Bean的困境:-
无缓存:由于
prototype
Bean每次都需要新实例,Spring无法通过缓存提前暴露一个固定的早期引用。 -
无限递归:当创建
prototype
的Bean A时,它需要注入Bean B的新实例;而创建Bean B时,又需要注入Bean A的新实例。这个过程会无限循环,直到抛出异常。
-
⑶.Spring的异常触发逻辑
-
循环检测:Spring在创建Bean时,会记录当前正在创建的Bean名称。如果发现某个Bean的创建过程中再次请求自身(或形成环形依赖),则触发异常。
-
示例流程:
-
请求
prototype
Bean A → 开始创建A。 -
A需要注入
prototype
Bean B → 请求创建B。 -
B需要注入
prototype
Bean A → 再次请求创建A。 -
Spring检测到A已经在创建中(但无法通过缓存复用),抛出
BeanCurrentlyInCreationException
。
-
⑷.为何单例Bean可以解决循环依赖,而prototype
不行?
-
单例Bean:整个容器中只有唯一实例,通过缓存可提前暴露引用,依赖注入的是同一个对象。
-
prototype
Bean:每次注入都是新实例,无法复用已存在的引用,导致无限循环。
5. 代码示例与现象
假设以下两个prototype
Bean互相依赖:
@Component
@Scope("prototype")
public class A {
private B b;
@Autowired
public void setB(B b) { this.b = b; }
}
@Component
@Scope("prototype")
public class B {
private A a;
@Autowired
public void setA(A a) { this.a = a; }
}
异常信息:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
6. 解决方案
-
避免循环依赖:重新设计Bean之间的关系,移除不必要的双向依赖。
-
使用
@Lazy
延迟注入:对其中一个Bean的依赖添加@Lazy
注解,延迟实际代理对象的创建。@Component @Scope("prototype") public class A { @Autowired @Lazy // 延迟注入B的代理对象 private B b; }
-
改用单例作用域:如果业务允许,将其中一个Bean改为单例,利用Spring的缓存机制解决循环依赖。
⑺.总结
prototype
作用域的Bean在循环依赖时失败,本质是因为Spring无法通过缓存复用未初始化的对象,导致无限递归创建新实例。而单例Bean的缓存机制和三级缓存设计使得循环依赖得以解决。在设计prototype
Bean时,需特别注意依赖关系的合理性。
4.singleton下的构造注入产生的循环依赖
我们再来测试一下singleton + 构造注入的方式下,spring是否能够解决这种循环依赖。
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 +
'}';
}
}
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="h" scope="singleton" class="com.spring6.bean2.Husband">
<constructor-arg index="0" value="张三"></constructor-arg>
<constructor-arg index="1" ref="w"></constructor-arg>
</bean>
<bean id="w" scope="singleton" class="com.spring6.bean2.Wife">
<constructor-arg index="0" value="小花"></constructor-arg>
<constructor-arg index="1" ref="h"></constructor-arg>
</bean>
</beans>
Test
@Test
public void testCD2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
Husband husbandBean = applicationContext.getBean("h", Husband.class);
System.out.println(husbandBean);
Wife wifeBean = applicationContext.getBean("w", Wife.class);
System.out.println(wifeBean);
}
和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的。
为什么呢?
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。
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对象提前曝光。
再分析下面的源码:
源码分析: 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对象都会对应一个单例工厂对象。 这个集合中存储的是,创建该单例对象时对应的那个单例工厂对象。
从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。
总结:
Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。
Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。
二、回顾反射机制
1.分析方法四要素
我们先来看一下,不使用反射机制调用一个方法需要几个要素的参与。 有一个这样的类:
SomeService类
public class SomeService {
public void doSome(){
System.out.println("public void doSome()执行。");
}
public String doSome(String s){
System.out.println("public String doSome(String s)执行。");
return s;
}
public String doSome(String s, int i){
System.out.println("public String doSome(String s, int i)执行。");
return s + i;
}
}
编写程序调用方法:
public class Test {
public static void main(String[] args) {
// 不使用反射机制调用这些方法
SomeService someService = new SomeService();
someService.doSome();
/**
* 分析:调用一个方法,当中含有几个要素?四要素。
* 第一要素:调用哪个对象
* 第二要素:调用哪个方法
* 第三要素:调用方法的时候传什么参数
* 第四要素:方法执行结束之后的返回结果
*
* 调用哪个对象的哪个方法,传什么参数,返回什么值。
*
* 即使是使用反射机制调用方法,也同样需要具备这四个要素。
*/
String s1 = someService.doSome("张三");
System.out.println(s1);
String s2 = someService.doSome("李四", 250);
System.out.println(s2);
}
}
通过以上代码可以看出,调用一个方法,一般涉及到4个要素:
-
调用哪个对象的
-
哪个方法
-
传什么参数
-
返回什么值
2.获取Method
要使用反射机制调用一个方法,首先你要获取到这个方法。 在反射机制中Method实例代表的是一个方法。那么怎么获取Method实例呢? 有这样一个类:
public class SomeService {
public void doSome(){
System.out.println("public void doSome()执行。");
}
public String doSome(String s){
System.out.println("public String doSome(String s)执行。");
return s;
}
public String doSome(String s, int i){
System.out.println("public String doSome(String s, int i)执行。");
return s + i;
}
}
我们如何获取到 doSome( )、doSome(String s)、doSome(String s, int i) 这三个方法呢? 要获取方法Method,首先你需要获取这个类Class。
Class<?> clazz = Class.forName("com.reflect.SomeService");
当拿到Class之后,调用getDeclaredMethod()方法可以获取到方法。 假如你要获取这个方法:
doSome(String s, int i)
Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class, int.class);
假如你要获取到这个方法:doSome(String s)
Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class);
获取一个方法,需要告诉Java程序,你要获取的方法的名字是什么,这个方法上每个形参的类型是什么。这样Java程序才能给你拿到对应的方法。
这样的设计也非常合理,因为在同一个类当中,方法是支持重载的,也就是说方法名可以一样,但参数列表一定是不一样的,所以获取一个方法需要提供方法名以及每个形参的类型。 假设有这样一个方法:
public void setAge(int age){
this.age = age;
}
你要获取这个方法的话,代码应该这样写:
Method setAgeMethod = clazz.getDeclaredMethod("setAge", int.class);
其中setAge是方法名,int.class是形参的类型。
如果要获取上面的doSome( )方法,代码应该这样写:
Method doSomeMethod = clazz.getDeclaredMethod("doSome");
因为这个方法形式参数的个数是0个。所以只需要提供方法名就行了。你学会了吗?
3.调用Method
要让一个方法调用的话,就关联到四要素了:
-
调用哪个对象的
-
哪个方法
-
传什么参数
-
返回什么值
public class SomeService {
public void doSome(){
System.out.println("public void doSome()执行。");
}
public String doSome(String s){
System.out.println("public String doSome(String s)执行。");
return s;
}
public String doSome(String s, int i){
System.out.println("public String doSome(String s, int i)执行。");
return s + i;
}
}
public class Test2 {
public static void main(String[] args) throws Exception{
// 使用反射机制怎么调用方法。
// 获取类
Class<?> clazz = Class.forName("com.reflect.SomeService");
// 获取方法
Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class, int.class);
// 调用方法
// 四要素:调用哪个对象、哪个方法、传什么参数、返回什么值。
// obj 要素:哪个对象
// doSomeMethod 要素:哪个方法
// "李四", 250 要素:传什么参数
// retValue 要素:返回什么值。
Object obj = clazz.newInstance();
Object retValue = doSomeMethod.invoke(obj, "李四", 250);
System.out.println(retValue);
}
}
4.假设你知道属性名
假设有这样一个类:
public class User {
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = 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 +
'}';
}
}
需求: 假设你现在已知以下信息: 1. 有这样一个类,类名叫做:com.reflect.User 2. 这个类符合javabean规范。属性私有化,对外提供公开的setter和getter方法。 3. 你还知道这个类当中有一个属性,属性的名字叫做 age 4. 并且你还知道age属性的类型是int类型。 请使用反射机制调用set方法,给User对象的age属性赋值。
public class Test4 {
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.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, 30);
System.out.println(obj);
}
}
三、手写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>
<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>
<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不是框架内置的。是将来使用我们框架的程序员提供的。
public class User {
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 "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class UserDao {
public void insert(){
System.out.println("Mysql数据库正在保存用户信息");
}
}
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
第三步:准备myspring.xml配置文件
将来在框架开发完毕之后,这个文件也是要删除的。因为这个配置文件的提供者应该是使用这个框架的程序员。 文件名随意,我们这里叫做:myspring.xml 文件放在类路径当中即可,我们这里把文件放到类的根路径下。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="user" class="org.example1.bean.User">
<property name="name" value="张三"></property>
<property name="age" value="30"></property>
<!--<property name="height" value="1.83"></property>-->
</bean>
<bean id="userDaoBean" class="org.example1.bean.UserDao"></bean>
<bean id="userService" class="org.example1.bean.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
使用value给简单属性赋值。使用ref给非简单属性赋值。
第四步:编写ApplicationContext接口
ApplicationContext接口中提供一个getBean()方法,通过该方法可以获取Bean对象。 注意包名:这个接口就是myspring框架中的一员了。
public interface ApplicationContext {
Object getBean(String beanName);
}
第五步:编写ClassPathXmlApplicationContext
ClassPathXmlApplicationContext是ApplicationContext接口的实现类。该类从类路径当中加载myspring.xml配置文件。
public class ClassPathXmlApplicationContext implements ApplicationContext{
public ClassPathXmlApplicationContext(String configLocation){
}
@Override
public Object getBean(String beanName) {
return null;
}
}
第六步:确定采用Map集合存储Bean
确定采用Map集合存储Bean实例。Map集合的key存储beanId,value存储Bean实例。Map<String,Object> 在ClassPathXmlApplicationContext类中添加Map<String,Object>属性。
并且在ClassPathXmlApplicationContext类中添加构造方法,该构造方法的参数接收myspring.xml文件。 同时实现getBean方法。
public class ClassPathXmlApplicationContext implements ApplicationContext{
private Map<String, Object> singletonObjects = new HashMap<>();
/**
* 解析myspring的配置文件,然后初始化所有的Bean对象。
* @param configLocation spring配置文件的路径。注意:使用ClassPathXmlApplicationContext,配置文件应当放到类路径下。
*/
public ClassPathXmlApplicationContext(String configLocation) {
// 解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObjects集合当中。
}
@Override
public Object getBean(String beanName) {
return null;
}
}
第七步:解析配置文件实例化所有Bean
在ClassPathXmlApplicationContext的构造方法中解析配置文件,获取所有bean的类名,通过反射机制调用无参数构造方法创建Bean。并且将Bean对象存放到Map集合中。
package org.example1.core;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClassPathXmlApplicationContext implements ApplicationContext {
private static final Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class);
private Map<String, Object> singletonObjects = new HashMap<>();
/**
* 解析myspring的配置文件,然后初始化所有的Bean对象。
* @param configLocation spring配置文件的路径。注意:使用ClassPathXmlApplicationContext,配置文件应当放到类路径下。
*/
public ClassPathXmlApplicationContext(String configLocation) {
try {
// 解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObjects集合当中。
// 这是dom4j解析XML文件的核心对象。
SAXReader reader = new SAXReader();
// 获取一个输入流,指向配置文件
InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);
// 读文件
Document document = reader.read(in);
// 获取所有的bean标签
List<Node> nodes = document.selectNodes("//bean");
// 遍历bean标签
nodes.forEach(node -> {
try {
// 向下转型的目的是为了使用Element接口里更加丰富的方法。
Element beanElt = (Element) node;
// 获取id属性
String id = beanElt.attributeValue("id");
// 获取class属性
String className = beanElt.attributeValue("class");
logger.info("beanName=" + id);
logger.info("beanClassName=" + className);
// 通过反射机制创建对象,将其放到Map集合中,提前曝光。
// 获取Class
Class<?> aClass = Class.forName(className);
// 获取无参数构造方法
Constructor<?> defaultCon = aClass.getDeclaredConstructor();
// 调用无参数构造方法实例化Bean
Object bean = defaultCon.newInstance();
// 将Bean曝光,加入Map集合
singletonObjects.put(id, bean);
// 记录日志
logger.info(singletonObjects.toString());
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
} // 这里补上了缺失的右花括号
}
@Override
public Object getBean(String beanName) {
return null;
}
}
第八步:测试能否获取到Bean
编写测试程序。
@Test
public void testMySpring(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
}
通过测试Bean已经实例化成功了,属性的值是null,这是我们能够想到的,毕竟我们调用的是无参数构造方法,所以属性都是默认值。 下一步就是我们应该如何给Bean的属性赋值呢?
第九步:给Bean的属性赋值
通过反射机制调用set方法,给Bean的属性赋值。 继续在ClassPathXmlApplicationContext构造方法中编写代码。
package org.example1.core;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClassPathXmlApplicationContext implements ApplicationContext{
private static final Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class);
private Map<String, Object> singletonObjects = new HashMap<>();
/**
* 解析myspring的配置文件,然后初始化所有的Bean对象。
* @param configLocation spring配置文件的路径。注意:使用ClassPathXmlApplicationContext,配置文件应当放到类路径下。
*/
public ClassPathXmlApplicationContext(String configLocation) {
try {
// 解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObjects集合当中。
// 这是dom4j解析XML文件的核心对象。
SAXReader reader = new SAXReader();
// 获取一个输入流,指向配置文件
InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);
// 读文件
Document document = reader.read(in);
// 获取所有的bean标签
List<Node> nodes = document.selectNodes("//bean");
// 遍历bean标签
nodes.forEach(node -> {
try {
// 向下转型的目的是为了使用Element接口里更加丰富的方法。
Element beanElt = (Element) node;
// 获取id属性
String id = beanElt.attributeValue("id");
// 获取class属性
String className = beanElt.attributeValue("class");
logger.info("beanName=" + id);
logger.info("beanClassName="+className);
// 通过反射机制创建对象,将其放到Map集合中,提前曝光。
// 获取Class
Class<?> aClass = Class.forName(className);
// 获取无参数构造方法
Constructor<?> defaultCon = aClass.getDeclaredConstructor();
// 调用无参数构造方法实例化Bean
Object bean = defaultCon.newInstance();
// 将Bean曝光,加入Map集合
singletonObjects.put(id, bean);
// 记录日志
logger.info(singletonObjects.toString());
} catch (Exception e) {
e.printStackTrace();
}
});
// 再次重新把所有的bean标签遍历一次,这一次主要是给对象的属性赋值。
nodes.forEach(node -> {
try {
Element beanElt = (Element) node;
// 获取id
String id = beanElt.attributeValue("id");
// 获取className
String className = beanElt.attributeValue("class");
// 获取Class
Class<?> aClass = Class.forName(className);
// 获取该bean标签下所有的属性property标签
List<Element> propertys = beanElt.elements("property");
// 遍历所有的属性标签
propertys.forEach(property -> {
try {
// 获取属性名
String propertyName = property.attributeValue("name");
// 获取属性类型
Field field = aClass.getDeclaredField(propertyName);
logger.info("属性名:" + propertyName);
// 获取set方法名
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
// 获取set方法
Method setMethod = aClass.getDeclaredMethod(setMethodName, field.getType());
// 获取具体的值
String value = property.attributeValue("value"); // "30"
Object actualValue = null; // 真值
String ref = property.attributeValue("ref");
if (value != null) {
// 说明这个值是简单类型
// 调用set方法(set方法没有返回值)
// 我们myspring框架声明一下:我们只支持这些类型为简单类型
// byte short int long float double boolean char
// Byte Short Integer Long Float Double Boolean Character
// String
// 获取属性类型名
String propertyTypeSimpleName = field.getType().getSimpleName();
switch (propertyTypeSimpleName) {
case "byte":
actualValue = Byte.parseByte(value);
break;
case "short":
actualValue = Short.parseShort(value);
break;
case "int":
actualValue = Integer.parseInt(value);
break;
case "long":
actualValue = Long.parseLong(value);
break;
case "float":
actualValue = Float.parseFloat(value);
break;
case "double":
actualValue = Double.parseDouble(value);
break;
case "boolean":
actualValue = Boolean.parseBoolean(value);
break;
case "char":
actualValue = value.charAt(0);
break;
case "Byte":
actualValue = Byte.valueOf(value);
break;
case "Short":
actualValue = Short.valueOf(value);
break;
case "Integer":
actualValue = Integer.valueOf(value);
break;
case "Long":
actualValue = Long.valueOf(value);
break;
case "Float":
actualValue = Float.valueOf(value);
break;
case "Double":
actualValue = Double.valueOf(value);
break;
case "Boolean":
actualValue = Boolean.valueOf(value);
break;
case "Character":
actualValue = Character.valueOf(value.charAt(0));
break;
case "String":
actualValue = value;
}
setMethod.invoke(singletonObjects.get(id), actualValue);
}
if (ref != null) {
// 说明这个值是非简单类型
// 调用set方法(set方法没有返回值)
setMethod.invoke(singletonObjects.get(id), singletonObjects.get(ref));
}
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String beanName) {
return singletonObjects.get(beanName);
}
}
重点处理:当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>
<dependencies>
<!--用myspring框架,需要引入依赖-->
<dependency>
<groupId>org.example1</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
public class Vip {
private String name;
private int age;
private double height;
@Override
public String toString() {
return "Vip{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setHeight(double height) {
this.height = height;
}
}
public class OrderService {
private OrderDao orderDao;
public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}
public void generate(){
orderDao.insert();
}
}
public class OrderDao {
public void insert(){
System.out.println("正在保存订单信息....");
}
}
编写myspring.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="vip" class="com.myspring.bean.Vip">
<property name="name" value="jackson"></property>
<property name="age" value="30"></property>
<property name="height" value="1.83"></property>
</bean>
<bean id="orderDaoBean" class="com.myspring.bean.OrderDao"></bean>
<bean id="orderService" class="com.myspring.bean.OrderService">
<property name="orderDao" ref="orderDaoBean"/>
</bean>
</beans>
编写测试程序
@Test
public void testMySpring(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
Object vip = applicationContext.getBean("vip");
System.out.println(vip);
OrderService orderService = (OrderService) applicationContext.getBean("orderService");
orderService.generate();
}
四、Spring IoC注解式开发
1.回顾注解
注解的存在主要是为了简化XML的配置。Spring6倡导全注解开发。 我们来回顾一下:
-
第一:注解怎么定义,注解中的属性怎么定义?
-
第二:注解怎么使用?
-
第三:通过反射机制怎么读取注解?
注解怎么定义,注解中的属性怎么定义?
// 标注注解的注解,叫做元注解。@Target注解用来修饰@Component可以出现的位置。
// 以下表示@Component注解可以出现在类上、属性上。
//@Target(value = {ElementType.TYPE, ElementType.FIELD})
// 以下表示@Component注解可以出现在类上
//@Target(value = {ElementType.TYPE})
// 使用某个注解的时候,如果注解的属性名是value的话,value可以省略。
//@Target({ElementType.TYPE})
// 使用某个注解的时候,如果注解的属性值是数组,并且数组中只有一个元素,大括号可以省略。
@Target(ElementType.TYPE)
// @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。
注解怎么使用?
//@Component(属性名 = 属性值, 属性名 = 属性值, 属性名 = 属性值....)
//@Component(value = "userBean")
// 如果属性名是value,value可以省略。
@Component("userBean")
public class User {
// 编译器报错,不能出现在这里。
//@Component(value = "test")
//private String name;
}
用法简单,语法格式:@注解类型名(属性名=属性值, 属性名=属性值, 属性名=属性值......)
userBean为什么使用双引号括起来,因为value属性是String类型,字符串。
另外如果属性名是value,则在使用的时候可以省略属性名
通过反射机制怎么读取注解?
//@Component(属性名 = 属性值, 属性名 = 属性值, 属性名 = 属性值....)
//@Component(value = "userBean")
// 如果属性名是value,value可以省略。
@Component("userBean")
public class User {
// 编译器报错,不能出现在这里。
//@Component(value = "test")
//private String name;
}
public class ReflectAnnotationTest1 {
public static void main(String[] args) throws Exception{
// 通过反射机制怎么读取注解
// 获取类
Class<?> aClass = Class.forName("com.bean.User");
// 判断类上面有没有这个注解
if (aClass.isAnnotationPresent(Component.class)) {
// 获取类上的注解
Component annotation = aClass.getAnnotation(Component.class);
// 访问注解属性
System.out.println(annotation.value());
}
}
}
接下来,我们来写一段程序,当Bean类上有Component注解时,则实例化Bean对象,如果没有,则不实例化对象。
我们准备两个Bean,一个上面有注解,一个上面没有注解。
@Component("vipBean")
public class Vip {
}
public class Order {
}
假设我们现在只知道包名:com.bean。至于这个包下有多少个Bean我们不知道。哪些Bean上有注解,哪些Bean上没有注解,这些我们都不知道,如何通过程序全自动化判断。
package com.client;
import com.annotation.Component;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* @className ComponentScan
* @since 1.0
**/
public class ComponentScan {
public static void main(String[] args){
Map<String,Object> beanMap = new HashMap<>();
// 目前只知道一个包的名字,扫描这个包下所有的类,当这个类上有@Component注解的时候,实例化该对象,然后放到Map集合中。
String packageName = "com.bean";
// 开始写扫描程序。
// . 这个正则表达式代表任意字符。这里的"."必须是一个普通的"."字符。不能是正则表达式中的"."
// 在正则表达式当中怎么表示一个普通的"."字符呢?使用 \. 正则表达式代表一个普通的 . 字符。
String packagePath = packageName.replaceAll("\\.", "/");
System.out.println(packagePath);
// com是在类的根路径下的一个目录。
URL url = ClassLoader.getSystemClassLoader().getResource(packagePath);
String path = url.getPath();
System.out.println(path);
System.out.println("==========================");
// 获取一个绝对路径下的所有文件
File file = new File(path);
File[] files = file.listFiles();
Arrays.stream(files).forEach(f -> {
try {
System.out.println(f.getName());
System.out.println(f.getName().split("\\.")[0]);
String className = packageName + "." + f.getName().split("\\.")[0];
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);
}
}
2.声明Bean的注解
负责声明Bean的注解,常见的包括四个:
-
@Component
-
@Controller
-
@Service
-
@Repository
源码如下:
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的名字。
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">
<context:component-scan base-package="com.spring6.bean"/>
</beans>
第四步:在Bean类上使用注解
@Component(value = "userBean")
public class User {
}
@Controller("vipBean") // 如果属性名是value的话,这个属性名可以省略。
public class Vip {
}
@Service(value="orderBean") // 如果你把整个value属性全部省略了,bean有没有默认的名称?有:类名首字母变小写就是bean的名字。
public class Order {
}
@Repository(value = "studentBean")
public class Student {
}
编写测试程序:
@Test
public void testBeanComponent(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println(userBean);
Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vipBean);
Order orderBean = applicationContext.getBean("orderBean", Order.class);
System.out.println(orderBean);
Student studentBean = applicationContext.getBean("studentBean", Student.class);
System.out.println(studentBean);
}
如果注解的属性名是value,那么value是可以省略的。
@Repository( "studentBean")
public class Student {
}
@Controller("vipBean") // 如果属性名是value的话,这个属性名可以省略。
public class Vip {
}
test
@Test
public void testBeanComponent(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println(userBean);
Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vipBean);
Order orderBean = applicationContext.getBean("orderBean", Order.class);
System.out.println(orderBean);
Student studentBean = applicationContext.getBean("studentBean", Student.class);
System.out.println(studentBean);
}
照常运行:
如果把value属性彻底去掉,spring会被Bean自动取名吗?会的。并且默认名字的规律是:Bean类名首字母小写即可。
@Repository
public class Student {
}
@Service // 如果你把整个value属性全部省略了,bean有没有默认的名称?有:类名首字母变小写就是bean的名字。
public class Order {
}
Test
@Test
public void testBeanComponent(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
//Order orderBean = applicationContext.getBean("orderBean", Order.class);
Order orderBean = applicationContext.getBean("order", Order.class);
System.out.println(orderBean);
//Student studentBean = applicationContext.getBean("studentBean", Student.class);
Student studentBean = applicationContext.getBean("student", Student.class);
System.out.println(studentBean);
}
如果是多个包怎么办?有两种解决方案:
-
第一种:在配置文件中指定多个包,用逗号隔开。
-
第二种:指定多个包的共同父包。
<?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"/>-->
<!--多个包,使用逗号隔开。-->
<!--<context:component-scan base-package="com.powernode.spring6.bean,com.powernode.spring6.dao"/>-->
<!--多个包,也可以指定这多个包共同的父包,但是这肯定要牺牲一部分效率。-->
<context:component-scan base-package="com.powernode.spring6"/>
</beans>
4.选择性实例化Bean
假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与Bean管理,其他的都不实例化。这应该怎么办呢?
⑴.第一种解决方案
@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的无参数构造方法执行");
}
}
我只想实例化bean2包下的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.spring6.bean2包下所有的带有声明Bean的注解全部失效。@Component @Controller @Service @Repository全部失效。
-->
<context:component-scan base-package="com.spring6.bean2" use-default-filters="false">
<!--只有@Repository @Service 被包含进来,生效。-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
Test
@Test
public void testChoose(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-choose.xml");
}
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进行实例化。
⑵.第二种解决方案
<?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="true"
如果这个属性的值是true,表示com.powernode.spring6.bean2下的所有的带有声明Bean的注解全部生效。
use-default-filters="true" 默认值就是true,不用写。
-->
<context:component-scan base-package="com.spring6.bean2">
<!--@Controller注解失效-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
也可以将use-default-filters设置为true(不写就是true),并且采用exclude-filter方式排出哪些注解标注的Bean不参与实例化:
5.负责注入的注解
@Component @Controller @Service @Repository 这四个注解是用来声明Bean的,声明后这些Bean将被实例化。接下来我们看一下,如何给Bean的属性赋值。给Bean属性赋值需要用到这些注解:
-
@Value
-
@Autowired
-
@Qualifier
-
@Resource
⑴.@Value
当属性的类型是简单类型时,可以使用@Value注解进行注入。
MyDataSource类
package com.spring6.bean3;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
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;
/**
* @className MyDataSource
* @since 1.0
**/
@Component
public class MyDataSource implements DataSource {
@Value(value = "com.mysql.cj.jdbc.Driver")
private String driver;
@Value("jdbc:mysql://localhost:3306/spring6")
private String url;
@Value("root")
private String username;
@Value("123456")
private String password; // 使用@Value注解注入的话,可以用在属性上,并且可以不提供setter方法。
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;
}
}
<?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.spring6.bean3"/>
</beans>
Test
@Test
public void testDIByAnnotation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");
MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);
System.out.println(myDataSource);
}
把set方法注释掉,依旧可以运行
使用@Value注解注入的话,可以用在属性上,并且可以不提供setter方法。
通过以上代码可以发现,我们并没有给属性提供setter方法,但仍然可以完成属性赋值。 如果提供setter方法,并且在setter方法上添加@Value注解,可以完成注入吗?尝试一下:
@Component
public class Product {
private String name;
private int age;
// @Value注解也可以使用在方法上。
@Value("隔壁老王2")
public void setName(String name) {
this.name = name;
}
@Value("33")
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Test
@Test
public void testDIByAnnotation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");
MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);
System.out.println(myDataSource);
Product product = applicationContext.getBean("product", Product.class);
System.out.println(product);
}
通过测试可以得知,@Value注解可以直接使用在属性上,也可以使用在setter方法上。都是可以的。都可以完成属性的赋值。 为了简化代码,以后我们一般不提供setter方法,直接在属性上使用@Value注解完成属性赋值。
出于好奇,我们再来测试一下,是否能够通过构造方法完成注入:
@Component
public class Product {
private String name;
private int age;
public Product(@Value("隔壁老王3") String name, @Value("42") int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Test
@Test
public void testDIByAnnotation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");
MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);
System.out.println(myDataSource);
Product product = applicationContext.getBean("product", Product.class);
System.out.println(product);
}
通过测试得知:@Value注解可以出现在属性上、setter方法上、以及构造方法的形参上。可见Spring给我们提供了多样化的注入。太灵活了。
⑵@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注解:
OrderDao接口
public interface OrderDao {
void insert();
}
OrderDaoImplForMySQL实现类
<?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="org.dao,org.service"/>
</beans>
@Repository("orderDaoImplForMySQL")
public class OrderDaoImplForMySQL implements OrderDao {
@Override
public void insert() {
System.out.println("MySQL数据库正在保存订单信息...");
}
}
OrderService类
@Service("orderService")
public class OrderService {
// @Autowired注解使用的时候,不需要指定任何属性,直接使用这个注解即可。
// 这个注解的作用是根据类型byType进行自动装配。
@Autowired
private OrderDao orderDao;
public void generate(){
orderDao.insert();
}
}
Test
@Test
public void testAutowired(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowired.xml");
org.service.OrderService orderService = applicationContext.getBean("orderService", org.service.OrderService.class);
orderService.generate();
}
当存在多个相同类型的Bean时,无法自动装配
添加OrderDaoImplOracle类
/**
* @className OrderDaoImplForOracle
* @since 1.0
**/
@Repository("orderDaoImplForOracle")
public class OrderDaoImplForOracle implements OrderDao {
@Override
public void insert() {
System.out.println("Oracle数据库正在保存订单信息...");
}
}
解决方法:根据名字进行装配,Qualifier
@Service("orderService")
public class OrderService {
// @Autowired注解使用的时候,不需要指定任何属性,直接使用这个注解即可。
// 这个注解的作用是根据类型byType进行自动装配。
//@Autowired
//private OrderDao orderDao;
// 如果想解决以上问题,只能根据名字进行装配。
// @Autowired和@Qualifier联合使用,可以根据名字进行装配。
@Autowired
@Qualifier("orderDaoImplForOracle")
//@Qualifier("orderDaoImplForMySQL")
private OrderDao orderDao;
public void generate(){
orderDao.insert();
}
}
Test
@Test
public void testAutowired(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowired.xml");
org.service.OrderService orderService = applicationContext.getBean("orderService", org.service.OrderService.class);
orderService.generate();
}
以上构造方法和setter方法都没有提供,经过测试,仍然可以注入成功。
接下来,再来测试一下@Autowired注解出现在setter方法上:
@Service("orderService")
public class OrderService {
private OrderDao orderDao;
@Autowired
public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}
public void generate(){
orderDao.insert();
}
}
我们再来看看能不能出现在构造方法上:
@Service("orderService")
public class OrderService {
private OrderDao orderDao;
@Autowired
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
public void generate(){
orderDao.insert();
}
}
再来看看,这个注解能不能只标注在构造方法的形参上:
@Service("orderService")
public class OrderService {
private OrderDao orderDao;
public OrderService(@Autowired OrderDao orderDao) {
this.orderDao = orderDao;
}
public void generate(){
orderDao.insert();
}
}
还有更劲爆的,当有参数的构造方法只有一个时,@Autowired注解可以省略。
@Service("orderService")
public class OrderService {
private OrderDao orderDao;
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
public void generate(){
orderDao.insert();
}
}
当然,如果有多个构造方法,@Autowired肯定是不能省略的。
总结:
@Autowired注解可以出现在:属性上、构造方法上、构造方法的参数上、setter方法上。
当带参数的构造方法只有一个,@Autowired注解可以省略。
@Autowired注解默认根据类型注入。如果要根据名称注入的话,需要配合@Qualifier注解一起使用。
⑶@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需要引入以下依赖。】
<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.*包名了。)
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@Resource注解的源码如下:
测试一下:
StudentDao接口
public interface StudentDao {
void deleteById();
}
/**
* @className StudentDaoImplForMySQL
* @since 1.0
**/
@Repository("studentDaoImplForMySQL")
public class StudentDaoImplForMySQL implements StudentDao {
@Override
public void deleteById() {
System.out.println("mysql数据库正在删除学生信息");
}
}
/**
* @className StudentService
* @since 1.0
**/
@Service("studentService")
public class StudentService {
@Resource(name = "studentDaoImplForMySQL")
private StudentDao studentDao;
public void deleteStudent(){
studentDao.deleteById();
}
}
<?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="cn.dao,cn.service"/>
</beans>
Test
@Test
public void testResource(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-resource.xml");
StudentService studentService = applicationContext.getBean("studentService", StudentService.class);
studentService.deleteStudent();
}
测试Resource注解能不能出现在set方法上,做以下修改
/**
* @className StudentService
* @since 1.0
**/
@Service("studentService")
public class StudentService {
private StudentDao studentDao;
@Resource(name = "studentDaoImplForMySQL")
public void setStudentDao(StudentDao studentDao) {
this.studentDao = studentDao;
}
public void deleteStudent(){
studentDao.deleteById();
}
}
不能出现在构造方法上,直接报错
当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
@Service("studentService")
public class StudentService {
@Resource
private StudentDao studentDao;
public void deleteStudent(){
studentDao.deleteById();
}
}
@Repository("studentDao")
public class StudentDaoImplForMySQL implements StudentDao {
@Override
public void deleteById() {
System.out.println("mysql数据库正在删除学生信息");
}
}
但是根据类型注入在有多个Bean时就不行了
增加一个类
@Repository
public class StudentDaoImplForOracle implements StudentDao {
@Override
public void deleteById() {
System.out.println("oracle正在删除student。。。。");
}
}
6.全注解式开发
所谓的全注解开发就是不再使用spring配置文件了。写一个配置类来代替配置文件。
package cn;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"cn.dao", "cn.service"})
public class Spring6Config {
}
编写测试程序:不再new ClassPathXmlApplicationContext()对象了。
@Test
public void testNoXML(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spring6Config.class);
StudentService studentService = context.getBean("studentService", StudentService.class);
studentService.deleteStudent();
}