九、Bean的循环依赖问题
9.1 什么是Bean的循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
⽐如:丈夫类Husband,妻⼦类Wife。Husband中有Wife的引⽤。Wife中有Husband的引⽤。
![](https://i-blog.csdnimg.cn/blog_migrate/0376b85b2c315003c2d735808d5072db.jpeg)
9.2 singleton下的set注⼊产⽣的循环依赖
Husband类:
//丈夫类
public class Husband {
private String name;
private Wife wife;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
Wife类:
//妻子类
public class Wife {
private String name;
private Husband husband;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
spring.xml文件:
<!--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>
@Test
public void testCD(){
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);
// Husband{name='猪猪侠', wife=小菲菲}
// Wife{name='小菲菲', husband=猪猪侠}
}
在singleton + set注⼊的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
9.3 prototype下的set注⼊产⽣的循环依赖
spring.xml文件:
<!--在prototype + setter模式下的循环依赖,存在问题,会出现异常!-->
<!-- Bean不会提前曝光,在调用getBean时创建,但是每次调用时又会创建新的对象-->
<!--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>
执⾏测试程序:发⽣了异常,异常信息如下:
Caused by: org.springframework.beans.factory.
BeanCurrentlyInCreationException
: ......
当循环依赖的
所有Bean
的scope="prototype"的时候,产⽣的循环依赖,Spring是⽆法解决的,会出现
BeanCurrentlyInCreationException
异常。原因是:
![](https://i-blog.csdnimg.cn/blog_migrate/9b489c9473b7faebbf82c6e105cf0231.png)
以上两个Bean,如果其中⼀个是singleton,另⼀个是prototype,是没有问题的。
<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="singleton">
<property name="name" value="小菲菲"/>
<property name="husband" ref="husbandBean"/>
</bean>
@Test
public void testCD(){
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);
// Husband{name='猪猪侠', wife=小菲菲}
// Wife{name='小菲菲', husband=猪猪侠}
}
9.4 singleton下的构造注⼊产⽣的循环依赖
测试⼀下singleton + 构造注⼊的⽅式下,spring是否能够解决这种循环依赖。
Husband类:
//丈夫类
public class Husband {
private String name;
private Wife wife;
public String getName() {
return name;
}
public Husband(String name, Wife wife) {
this.name = name;
this.wife = wife;
}
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}
Wife类:
//妻子类
public class Wife {
private String name;
private Husband husband;
public String getName() {
return name;
}
public Wife(String name, Husband husband) {
this.name = name;
this.husband = husband;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
spring2.xml文件:
<!--构造注入,这种循环依赖有没有问题?-->
<!--注意:基于构造注入的方式下产生的循环依赖也是无法解决的,所以编写代码时一定要注意。-->
<bean id="h" class="com.powernode.spring6.bean2.Husband">
<constructor-arg index="0" value="喜羊羊"></constructor-arg>
<constructor-arg index="1" ref="w"></constructor-arg>
</bean>
<bean id="w" class="com.powernode.spring6.bean2.Wife">
<constructor-arg index="0" value="美羊羊"></constructor-arg>
<constructor-arg index="1" ref="h"></constructor-arg>
</bean>
@Test
public void testCD2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
Husband h = applicationContext.getBean("h", Husband.class);
Wife w = applicationContext.getBean("w", Wife.class);
System.out.println(h);
System.out.println(w);
//报错
//org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'h' defined in class path resource
}
主要原因是因为通过构造⽅法注⼊导致的:因为构造⽅法注⼊会导致
实例化对象的过程
和
对象属性赋值的过程
没有分离开,必须在⼀起完成导致的。
9.5 Spring解决循环依赖的机理
Spring为什么可以解决set + singleton模式下循环依赖?
根本的原因在于:这种⽅式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
实例化Bean的时候:调⽤⽆参数构造⽅法来完成。
此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。
给Bean属性赋值的时候:调⽤setter⽅法来完成。
两个步骤是完全可以分离开去完成的,并且这两步不要求在同⼀个时间点上完成。
也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到⼀个集合当中(我们可以 称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调⽤setter⽅法给属性赋值。
这样就解决了循环依赖的问题。
那么在Spring框架底层源码级别上是如何实现的呢?请看:
![](https://i-blog.csdnimg.cn/blog_migrate/81138d0a09f744c0a54f63bff02c6ca1.jpeg)
在以上类中包含三个重要的属性:
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对象提前曝光。
![](https://i-blog.csdnimg.cn/blog_migrate/9d829754c1d035db3c18486e309cf057.jpeg)
再分析下⾯的源码:
从源码中可以看到,spring会先从⼀级缓存中获取Bean,如果获取不到,则从⼆级缓存中获取Bean,如
果⼆级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对
象获取Bean实例,这样就解决了循环依赖的问题。
总结:
Spring只能解决setter⽅法注⼊的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB⼜依赖
ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,⼜发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,⼜发现需要ClassA的属性,但此时的ClassA已经被提前曝光加⼊了正在创建的bean的缓存中,则⽆需创建新的的ClassA的实例,直接从缓存中获取即可。从⽽解决循环依赖问题。
⼗、回顾反射机制
10.1 分析⽅法四要素
我们先来看⼀下,不使⽤反射机制调⽤⼀个⽅法需要⼏个要素的参与。
SomeService类:
public class SomeService {
public void doSome(){
System.out.println("public void doSom()执行!");
}
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;
}
}
不使用反射机制调用方法,使用平常的方法调用:
Test类:
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("认识第几天了:", 1);
System.out.println(s2);
}
}
使用反射机制调用方法:
Test2类:
public class Test2 {
public static void main(String[] args) throws Exception{
//使用反射机制怎么调用方法
//获取类
Class<?> clazz = Class.forName("com.powernode.reflect.SomeService");
//获取方法
Method doSomeMethod = clazz.getDeclaredMethod("doSome", String.class, int.class);
//调用方法
//四要素:调用哪个对象,哪个方法,传什么参数,返回什么值。
//obj 要素:哪个参数
//doSomeMethod 要素:哪个方法
//"沸羊羊",36 要素;传什么参数
//retValue 要素:返回什么值
Object obj = clazz.newInstance();
Object retValue = doSomeMethod.invoke(obj, "沸羊羊", 36);
System.out.println(retValue);
// public String doSome(String s,int i)执行!
// 沸羊羊36
}
}
10.2 需求
需求: 假设你现在已知以下信息: 1. 有这样一个类,类名叫做:com.powernode.reflect.User 2. 这个类符合javabean规范。属性私有化,对外提供公开的setter和getter方法。 3. 你还知道这个类当中有一个属性,属性的名字叫做 age 4. 并且你还知道age属性的类型是int类型。 请使用反射机制调用set方法,给User对象的age属性赋值。
User类:
public class User {
private String name;
private int age;
public User(){
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
//后面跟get,set,toString方法
}
Test4类:
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.powernode.reflect.User";
String propertyName="age";
//通过反射机制嗲用setAge(int)方法
//获取类
Class<?> clazz = Class.forName(className);
//获取方法名
String setMethodName = "set"+propertyName.toUpperCase().charAt(0)+propertyName.substring(1);//setAge
//根据属性名,获取属性类型
//Field field = clazz.getDeclaredField(propertyName);
//获取方法
// Method setMethod = clazz.getDeclaredMethod(setMethodName, field.getType());
Method setMethod = clazz.getDeclaredMethod(setMethodName, int.class);
Object obj = clazz.newInstance();
//调用方法
setMethod.invoke(obj,666);
System.out.println(obj);//User{name='null', age=666}
}
}
⼗⼀、⼿写Spring框架
Spring IoC容器的实现原理:⼯⼚模式 + 解析XML + 反射机制。
我们给⾃⼰的框架起名为:myspring
第⼀步:创建模块myspring
打包⽅式采⽤jar,并且引⼊dom
4
j和jaxen的依赖,因为要使⽤它解析XML⽂件,还有junit依赖。
pom.xml:
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--dom4j是一个能够解析xml文件的java组件-->
<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>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
</dependencies>
第⼆步:准备好我们要管理的Bean
准备好我们要管理的Bean(
这些Bean在将来开发完框架之后是要删除的
)
注意包名,不要⽤org.myspringframework包,因为这些Bean不是框架内置的。是将来使⽤我们框架的程序员提供的。
User类:
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 +
'}';
}
}
UserDao类:
public class UserDao {
public void insert(){
System.out.println("mysql数据库正在保存用户信息.");
}
}
UserService类:
public class UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save(){
userDao.insert();
}
}
第三步:准备myspring.xml配置⽂件
将来在框架开发完毕之后,这个⽂件也是要删除的。因为这个配置⽂件的提供者应该是使⽤这个框架的程序员。
⽂件名随意,我们这⾥叫做:myspring.xml
⽂件放在类路径当中即可,我们这⾥把⽂件放到类的根路径下。
myspring.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<!--这个配置文件也是使用myspring框架开发人员提供的-->
<beans>
<bean id="user" class="com.powernode.myspring.bean.User">
<property name="name" value="猪猪侠"/>
<property name="age" value="36"/>
</bean>
<bean id="userDaoBean" class="com.powernode.myspring.bean.UserDao"></bean>
<bean id="userService" class="com.powernode.myspring.bean.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
</beans>
第四步:编写ApplicationContext接⼝
ApplicationContext接⼝中提供⼀个getBean()⽅法,通过该⽅法可以获取Bean对象。
注意包名:这个接⼝就是myspring框架中的⼀员了
//MySpring框架应用上下文接口
public interface ApplicationContext{
//根据bean的名称获取对应的bean对象
Object getBean(String beanName);
}
第五步:编写ClassPathXmlApplicationContext
ClassPathXmlApplicationContext是ApplicationContext接⼝的实现类。该类从类路径当中加载
myspring.xml配置⽂件。
刚开始的样子:
package org.myspringframework.core;
public class ClassPathXmlApplicationContext implements ApplicationContext{
@Override
public Object getBean(String beanName) {
return null;
}
}
最后的样子:
package org.springframework.core;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
public class ClassPathXmlApplicationContext implements ApplicationContext{
private static final Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class);
private Map<String,Object> singletonObjects = new HashMap<>();
//构造方法,解析myspring的配置文件,然后初始化所有的bean对象
//configLocation:spring配置文件的路径。注意:使用ClassPathXmlApplicationContext,配置文件应当放到类路径下
public ClassPathXmlApplicationContext(String configLocation){
try{
//解析myspring.xml文件,然后实例化Bean,将Bean存放到singletonObjects集合当中
//这是dom4j解析xml文件的核心对象
SAXReader saxReader = new SAXReader();
//获取一个输入流,指向配置文件
InputStream in = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);
//读文件
Document document = saxReader.read(in);
//获取所有的bean标签
List<Node> nodes = document.selectNodes("//bean");
//遍历bean标签
nodes.forEach(node -> {
try{
//System.out.println(node);
//向下转型,原因是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");
logger.info("属性名="+propertyName);
//获取属性类型
Field field = aClass.getDeclaredField(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);
}
}
第六步:打包发布
第七步:站在程序员⻆度使⽤myspring框架
⼗⼆、Spring IoC注解式开发
12.1 回顾注解
注解的存在主要是为了简化XML的配置。
Spring
6
倡导全注解开发
。
我们来回顾⼀下:
●第⼀:注解怎么定义,注解中的属性怎么定义?
●第⼆:注解怎么使⽤?
●第三:通过反射机制怎么读取注解?
注解怎么定义,注解中的属性怎么定义?
Component.java:
//自定义注解
//标注注解的注解,叫元注解 @Target注解用来修饰@Component可以出现的位置
// 以下表示@Component注解可以出现在类上,属性上
//@Target(value = {ElementType.TYPE})
// 使用某个注解的时候,如果注解的属性名是value的话,value可以省略。
//@Target({ElementType.TYPE})
// 使用某个注解的时候,如果注解的属性值是数组,并且数组中只有一个元素,大括号可以省略。
// @Retention 也是一个元注解。用来标注@Component注解最终保留在class文件当中,并且可以被反射机制读取。
@Target(value = {ElementType.TYPE})
@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。
注解怎么使⽤?
User类:
//@Component(属性名 = 属性值, 属性名 = 属性值, 属性名 = 属性值....)
//@Component(value = "userBean")
// 如果属性名是value,value可以省略。
@Component("UserBean")
public class User {
// 编译器报错,不能出现在这里。因为我们只让它在类上使用了
//@Component(value = "test")
//private String name;
}
⽤法简单,语法格式:@注解类型名(属性名=属性值, 属性名=属性值, 属性名=属性值......)
userBean为什么使⽤双引号括起来,因为value属性是String类型,字符串。
另外如果属性名是value,则在使⽤的时候可以省略属性名。
通过反射机制怎么读取注解?
ReflectAnnotationTest1类:
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());//UserBean
}
}
}
注解扫描原理
假设我们现在只知道包名:com.powernode.bean。⾄于这个包下有多少个Bean我们不知道。哪些Bean 上有注解,哪些Bean上没有注解,这些我们都不知道,如何通过程序全⾃动化判断。
接下来,我们来写⼀段程序,当Bean类上有Component注解时,则实例化Bean对象,如果没有,则不实例化对象。 我们准备两个Bean,⼀个上⾯有注解,⼀个上⾯没有注解。
User类:
//@Component(属性名 = 属性值, 属性名 = 属性值, 属性名 = 属性值....)
//@Component(value = "userBean")
// 如果属性名是value,value可以省略。
@Component("UserBean")
public class User {
// 编译器报错,不能出现在这里。因为我们只让它在类上使用了
//@Component(value = "test")
//private String name;
}
Vip类:
public class Vip {
}
Order类:
@Component("OrderBean")
public class Order {
}
ComponentScan类:
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/powernode/bean
//com是在类的根路径下的一个目录
URL url = ClassLoader.getSystemClassLoader().getResource(packagePath);
String path = url.getPath();
//由于在获取文件路径时可能会有中文或其他字符,所以我们需要异常处理,并且将其转换成UTF-8
try {
path = URLDecoder.decode(path, "UTF-8");
System.out.println(path);///D:/codes/IntelliJ IDEA/spring6/review-annotation/target/classes/com/powernode/bean
} catch (Exception e) {
e.printStackTrace();
}
//获取一个绝对路径下的所有文件
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];
//System.out.println(className);//com.powernode.bean.Order 三个如此
//通过反射机制解析注解
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);//{UserBean=com.powernode.bean.User@2e5d6d97, OrderBean=com.powernode.bean.Order@238e0d81}
}
}
12.2 声明Bean的注解
负责声明Bean的注解,常⻅的包括四个:
●@Component
●@Controller
●@Service
●@Repository
@Component注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}
@Controller注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
@Service注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
@Repository注解:
@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的名字。
![](https://i-blog.csdnimg.cn/blog_migrate/a0dc403078b26aaa160e8efc79b8fdc4.jpeg)
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类上使⽤注解
@Component(value = "userBean")
public class User {
}
编写测试程序:
@Test
public void testBeanComponent(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println(userBean);
}
如果注解的属性名是value,那么value是可以省略的。
如果把value属性彻底去掉,spring会被Bean⾃动取名吗?会的。并且默认名字的规律是:Bean类名⾸字⺟⼩写即可。
Order类:
@Service
public class Order {
}
Student类:
@Repository
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);
Student studentBean = applicationContext.getBean("student", Student.class);
System.out.println(studentBean);
Object orderBean = applicationContext.getBean("order", Order.class);
System.out.println(orderBean);
// com.powernode.spring6.bean.User@759d26fb
// com.powernode.spring6.bean.Vip@3c73951
// com.powernode.spring6.bean.Student@3d5c822d
// com.powernode.spring6.bean.Order@6f46426d
}
如果是多个包怎么办?有两种解决⽅案:
●第⼀种:在配置⽂件中指定多个包,⽤逗号隔开。
●第⼆种:指定多个包的共同⽗包。
<!--给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"/>
12.4 选择性实例化Bean
假设在某个包下有很多Bean,有的Bean上标注了Component,有的标注了Controller,有的标注了 Service,有的标注了Repository,现在由于某种特殊业务的需要,只允许其中所有的Controller参与 Bean管理,其他的都不实例化。这应该怎么办呢?
A类:
@Component
public class A {
public A(){
System.out.println("A 的无参数构造方法!");
}
}
@Controller
class B{
public B(){
System.out.println("B 的无参数构造方法!");
}
}
@Repository
class C{
public C(){
System.out.println("C 的无参数构造方法!");
}
}
@Service
class D{
public D(){
System.out.println("D 的无参数构造方法!");
}
}
@Controller
class E{
public E(){
System.out.println("E 的无参数构造方法!");
}
}
spring-choose.xml文件:
<!--
第一种解决方案:
use-default-filters="false"
如果这个属性是false,表示com.powernode.spring6.bean2包下所有的带有声明Bean的注解全部失效。
@Component @Controller @Service @Repository全部失效。
-->
<context:component-scan base-package="com.powernode.spring6.bean2" use-default-filters="false">
<!--只有@Controller被包含进来,生效-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--
第二种解决方案:
use-default-filters="true"
如果这个属性的值是true,表示com.powernode.spring6.bean2下的所有的带有声明Bean的注解全部生效。
use-default-filters="true" 默认值就是true,不用写。
-->
<context:component-scan base-package="com.powernode.spring6.bean2">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
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进⾏实例化。
也可以将use-default-filters设置为true(不写就是true),并且采⽤exclude-filter⽅式排出哪些注解标注的Bean不参与实例化。
12.5 负责注⼊的注解
@Component @Controller @Service @Repository 这四个注解是⽤来声明Bean的,声明后这些Bean将被实例化。接下来我们看⼀下,如何给Bean的属性赋值。给Bean属性赋值需要⽤到这些注解:
●@Value
●@Autowired
●@Qualifier
●@Resource
12.5.1 @Value
当属性的类型是简单类型时,可以使⽤@Value注解进⾏注⼊。
MyDataSource类:
@Component
public class MyDataSource implements DataSource {
//使用Value注解,可以用在属性上,可以不提供set方法
@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;
@Override
public String toString() {
return "MyDataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
Product类:
@Component
public class Product {
// @Value("猪猪侠")
// private String name;
// @Value("36")
// private int age;
private String name;
private int age;
public Product(@Value("懒羊羊") String name, @Value("16") int age) {
this.name = name;
this.age = age;
}
//@Value注解也可以使用在方法上
/* @Value("隔壁老王")
public void setName(String name) {
this.name = name;
}
@Value("45")
public void setAge(int age) {
this.age = age;
}*/
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
spring-di-annotation.xml文件:
<context:component-scan base-package="com.powernode.spring6.bean3"></context:component-scan>
@Test
public void testDIByAnnotation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-di-annotation.xml");
MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);
System.out.println(myDataSource);
//MyDataSource{driver='com.mysql.cj.jdbc.Driver', url='jdbc:mysql://localhost:3306/spring6', username='root', password='123456'}
Product product = applicationContext.getBean("product", Product.class);
System.out.println(product);
//Product{name='猪猪侠', age=36}
//Product{name='隔壁老王', age=45}
//Product{name='懒羊羊', age=16}
}
通过以上代码可以发现,我们并没有给属性提供setter⽅法,但仍然可以完成属性赋值。
通过测试得知:@Value注解可以出现在属性上、setter⽅法上、以及构造⽅法的形参上。可⻅Spring给我们提供了多样化的注⼊。
12.5.2 @Autowired与@Qualifier
@Autowired注解可以⽤来注⼊
⾮简单类型
。被翻译为:⾃动连线的,或者⾃动装配。
单独使⽤@Autowired注解,
默认根据类型装配
。【默认是byType】
看源码:
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETE
R, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
源码中有两处需要注意:
●第⼀处:该注解可以标注在哪⾥?
○构造⽅法上
○⽅法上
○形参上
○属性上
○注解上
●第⼆处:该注解有⼀个required属性,默认值是true,表示在注⼊的时候要求被注⼊的Bean必须是存在的,如果不存在则报错。如果required属性设置为false,表示注⼊的Bean存在或者不存在都没关系,存在的话就注⼊,不存在的话,也不报错。
OrderDao类:
public interface OrderDao {
void insert();
}
OrderDaoImplForMySql类:
@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();
}
}
spring-autowired.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"
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.powernode"></context:component-scan>
</beans>
@Test
public void testAutowired(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowired.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
//MySql数据库正在保存订单信息!
//Oracle数据库正在保存订单信息!
}
@Autowired和@Qualifier联合使用
当有多个实现类时,不知道要实例化哪一个类,此时就要使用@Qualifier来指定使用byName来确定实例化哪个类
OrderDaoImplForOracle类:
@Repository("orderDaoImplForOracle")
public class OrderDaoImplForOracle implements OrderDao {
@Override
public void insert() {
System.out.println("Oracle数据库正在保存订单信息!");
}
}
OrderService类:
@Service("orderService")
public class OrderService {
//@Autowired 注解使用的时候,不需要指定任何属性,直接使用这个注解即可
//这个注解的作用是根据类型byType进行自动装配
// @Autowired
// private OrderDao orderDao;
// public void generate(){
// orderDao.insert();
// }
@Autowired
// @Qualifier("orderDaoImplForMySQL")
@Qualifier("orderDaoImplForOracle")
private OrderDao orderDao;
public void generate(){
orderDao.insert();
}
}
以上构造⽅法和setter⽅法都没有提供,经过测试,仍然可以注⼊成功。
接下来,
再来测试⼀下@Autowired注解出现在setter⽅法上:
再来测试⼀下@Autowired注解出现在setter⽅法上:
我们再来看看能不能出现在构造⽅法上:
再来看看,这个注解能不能只标注在构造⽅法的形参上:
@Service("orderService")
public class OrderService {
//@Autowired 注解使用的时候,不需要指定任何属性,直接使用这个注解即可
//这个注解的作用是根据类型byType进行自动装配
// @Autowired
// private OrderDao orderDao;
// public void generate(){
// orderDao.insert();
// }
// @Autowired
@Qualifier("orderDaoImplForMySQL")
// @Qualifier("orderDaoImplForOracle")
private OrderDao orderDao;
//可以出现在set方法上
// @Autowired
// public void setOrderDao(OrderDao orderDao) {
// this.orderDao = orderDao;
// }
//可以出现在构造方法上
//@Autowired
// public OrderService(OrderDao orderDao) {
// this.orderDao = orderDao;
// }
//可以出现在形参上
// public OrderService(@Autowired OrderDao orderDao) {
// this.orderDao = orderDao;
// }
public void generate() {
orderDao.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当中,需要额外引⼊以下依赖:【
如果是JDK
8
的话不需要额外引⼊依赖。⾼于JDK11
或低于JDK
8
需要引⼊以下依赖。
】
如果你是Spring
6
+版本请使⽤这个依赖:
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
⼀定要注意:
如果你⽤Spring
6
,要知道Spring
6
不再⽀持JavaEE,它⽀持的是JakartaEE
9
。(Oracle 把JavaEE贡献给Apache了,Apache把JavaEE的名字改成JakartaEE了,⼤家之前所接触的所有的javax.*
包名统⼀修改为
jakarta.*包名了。)
如果你是spring
5
-版本请使⽤这个依赖:
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
StudentDao类:
public interface StudentDao {
void deleteById();
}
StudentDaoImplForMySql类:
给StudentDaoImplForMySql起名字studentDaoImplForMySql
@Repository("studentDaoImplForMySql")
public class StudentDaoImplForMySql implements StudentDao {
@Override
public void deleteById() {
System.out.println("mysql数据库正在删除学生信息!");
}
}
StudnetServie类:
@Service("studentService")
public class StudentService {
@Resource(name = "studentDaoImplForMySql")
private StudentDao studentDao;
public void deleteStudent(){
studentDao.deleteById();
}
}
spring-resource.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"
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.powernode"></context:component-scan>
</beans>
@Test
public void testResource(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-resource.xml");
StudentService studentService = applicationContext.getBean("studentService", StudentService.class);
studentService.deleteStudent();//mysql数据库正在删除学生信息!
}
我们把StudentDaoImplForMySql
的名字studentDaoImplForMySql
修改为studentDao,让这个Bean的名字和StudentService类中的StudentDao属性名⼀致:
@Repository("studentDao")
public class StudentDaoImplForMySql implements StudentDao {
@Override
public void deleteById() {
System.out.println("mysql数据库正在删除学生信息!");
}
}
@Service("studentService")
public class StudentService {
@Resource
private StudentDao studentDao;
public void deleteStudent(){
studentDao.deleteById();
}
}
通过测试得知,当@Resource注解使⽤时没有指定name的时候,还是根据name进⾏查找,这个name是 属性名。
接下来把StudentDaoImplForMySql
类的名字修改⼀下,此时匹配不到属性名,按照byType注入,某种类型的Bean只能有⼀个。
12.6 全注解式开发
所谓的全注解开发就是不再使⽤spring配置⽂件了。写⼀个配置类来代替配置⽂件。
Spring6Config类:
//编写一个类代替Spring框架的配置文件
@Configuration
@ComponentScan({"cn.powernode.dao","cn.powernode.Service"})
public class Spring6Config {
}
@Test
public void testNoXML(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Spring6Config.class);
StudentService studentService = context.getBean("studentService", StudentService.class);
studentService.deleteStudent();//mysql数据库正在删除学生信息!
}