Spring(万字详解版)
Posted bit_zhy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring(万字详解版)相关的知识,希望对你有一定的参考价值。
Spring
- 1.Spring是什么
- 2.Spring的创建和使用
- 3.使用Spring更简单的注册对象(使用注解)
- 4.使用Spring更简单的装配对象(对象注入)
- 5.Bean的作用域
- 6.Spring的执行流程
- 7.Bean的生命周期
1.Spring是什么
Spring就是一个包含众多工具方法的IoC容器
1)什么是容器?
容器就是存储某种东西的基本装置,在Java的学习过程中,我们曾接触过很多容器,例如数据结构中的List,Map等是数据存储的容器,Tomcat就是web程序的容器
2)什么是IoC?
IoC(Inversion of Control)也就是控制反转的意思 那么也就是说spring是个控制反转的容器,但是这样说还是有点不好理解,我们可以理解为控制权转换了的对象的容器,那什么是控制转换,我们为什么要使用控制转换呢?举个例子,比方说我们要实现一个Car类,那么我们需要实现车身,车底盘,车轮胎,由上而下是一个层层递进,层层依赖的关系
按照上图的逻辑我们写出的代码是
class Car
public void init()
Body body = new Body();
body.init();
class Body
public void init()
Bottom bottom = new Bottom();
bottom.init();
class Bottom
public void init()
Tires tires = new Tires();
tires.setSize(??);
class Tires
private int size;
public void setSize(int size)
this.size = size;
public class traditionalMethod
public static void main(String[] args)
Car car = new Car();
car.init();
当我们的业务需求是需要涉及不同尺寸的轮胎时,我们就需要层层调用直到底层的setSize,也就是我们需要在Bottom类中调用时需要传入??参数,但是由于我们每一个类都是在类自己内部new出自己的所需依赖,那么这个所需的依赖的控制权就相当于在我们手中,所有类中需要的参数都需要我们自己层层传输,我们在想要修改size时,就需要从顶层的car类中开始传入参数,一步一步的传到底层,这时我们需要修改涉及的代码就太多了,而且当代码的依赖逻辑更复杂时,或者需要给最底层的轮胎增删一些参数(例如增加颜色属性),我们需要更容易凌乱,出现这个问题的原因是上述代码耦合性太高了,我们不妨想,将new依赖对象这个操作交到外部,也就是将依赖对象的控制权交出去,当我们需要的时候直接拿对象来使用即可(也就是采用注入所需依赖对象的方法),那么这个时候就直接将依赖的对象当成了一个方法,当我们对所以来对象想要进行一些修改时,我们只需要在外部类创建时进行修改,然后在调用类中增删改参数即可,这时就完美的实现了程序的解耦合,用代码来实现就是
class Car2
private Body2 body2;
public Car2(Body2 body2)
this.body2 = body2;
public void run()
body2.init();
class Body2
private Bottom2 bottom2;
public Body2(Bottom2 bottom2)
this.bottom2 = bottom2;
public void init()
bottom2.init();
class Bottom2
private Tires2 tires2;
public Bottom2(Tires2 tires2)
this.tires2 = tires2;
public void init()
tires2.init();
class Tires2
private int size;
public Tires2(int size)
this.size = size;
public void init()
System.out.println("Size = " + size);;
public class IoCMethod
public static void main(String[] args)
Tires2 tires2 = new Tires2(20);
Bottom2 bottom2 = new Bottom2(tires2);
Body2 body2 = new Body2(bottom2);
Car2 car2 = new Car2(body2);
car2.run();
可以看到,在IoC方法中,我们将所有的依赖对象创建为上级类的属性,在构造方法中将在外边创建好的依赖对象当作参数传入,这时,如果我们仍然要修改最底层Tires的属性,我们只需要在Tires类中增删改参数,然后在main方法中将new Tires的参数增删改即可,剩下的几层代码是不需要改动的,因为我们将控制权发生了反转,这时new对象的顺序也是由下而上的,这样将依赖对象由自身new改为注入的方式,下级的控制权不是上级的了,那么无论下级如何改变上级都不需要改动,就很好的完成了解耦合
3)spring的主要作用
我们了解了容器和IoC之后,spring是什么就迎刃而解了,spring就是储存对象的容器,spring有着创建对象和销毁对象的权利,那么他的主要的作用就是:
1.可以将Bean(对象)存储到Spring容器当中
2.可以将Bean(对象)从Spring容器当中取出来
4)DI(Dependence Injection)依赖注入
所谓的“依赖注入”,就是指IoC容器在运行时将所需的依赖注入到类中,其实DI就是换了一个角度和IoC描述同样的一件事情
5)扩展:
a.IoC和DI有什么区别?
IoC是一种思想,DI是基于IoC这个思想的具体实现,我们代码耦合性高的解决思想是IoC,通过DI对象注入来事项IoC,类似于乐观锁和CAS的关系,乐观锁是一种思想,CAS是乐观锁的具体实现
b.spring是什么?spring的作用是什么?
spring就是包含有众多工具和方法的IoC容器,其主要作用是存储Bean和从其中取出Bean
2.Spring的创建和使用
1)spring的创建
1)先创建一个maven项目
2)添加spring框架支持(spring-context/spring-beans)
3)创建一个启动类并添加main,作为测试类看是否引入正常
2)将Bean存储(注册)到spring中(3步/2步)
如果是第一次添加对象,则需要先创建spring的配置文件(一般命名为spring-config.xml,在resources目录下),在spring-config.xml文件中创建bean标签,设置标签的id属性(默认命名格式为小驼峰)和class属性,id属性就是我们将对象注册入spring后在spring容器中的名字,class属性就是我们要注入的对象的路径,最终spring会以Map<String[beanName],Object> 来存储数据 也就是键值对的形式 id对应的是key,class对应的是value
3)使用容器中的Bean(对象)
1.得到spring的上下文对象
我们使用ApplicationContext类来取到spring的上下文对象,实例化其子类的ClassPathXmlApplicationContext,传入参数为刚刚创建的配置文件名(推荐是spring-config.xml),这样我们的进程就会扫描配置文件以及其中的bean标签,将bean注入到容器中
在这里我们也可以使用另一个类BeanFactory来获取上下文对象,这里需要new的是其子类xmlbeanfactory 同时xml对象中需要一个类资源文件ClassPathrResource其需要的参数也是配置文件名
2.获取spring中的bean
我们通过上边创建的(两个皆可)上下文对象提供的getBean()方法来获取到容器中的bean,传入的参数是注册bean时传入的id属性内容(beanName)
,需要注意的是getBean返回的对象类型是Object类,我们需要将其强转为bean本身的类型,但是这种方法有一个缺点,因为我们是直接传入字符串进去,那么如果我们所传入的beanName写错了等等,就会返回空的对象,我们在进行强制类型转换时就会报空指针异常
我们也可以通过getBean的重载方法来取到bean
第二个是传入对象的类型(类对象)
但是这种方法有一个缺点,就是如果我们将User这个类的实例化对象注册了两次的话,这种方法就会报错
Exception in thread “main” org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘beans.User’ available: expected single matching bean but found 2: user,user2
上述是编译器提示的异常信息,可以看到它提示我们user并不是single bean
那么我们就可以用第三种getBean,这一种将上述两种结合,是我们最推荐的方法
这种方法既可以防止第一种情况种出现的空指针异常(不涉及强转),也可以防止第二种方式非唯一的bean异常,因此我们一般推荐使用第三种
3.调用获取到的对象的方法
4.扩展
1)ApplicationContext 和 BeanFactory 的异同:
同:两种方法都可以获取到spring的上下文对象,也提供了getBean方法从而具体的获取到bean对象
异:
1.ApplicationContext 属于BeanFactory的子类,那么我们都知道,子类是继承了父类的所有非私有方法和属性的,但是父类并没有子类的属性方法,因此App类比BeanFact类要提供了更多的操作bean的方法,而不仅仅是简单的getBean,比如国际化的支持,资源访问的支持以及事件和传播等方面的支持
2.从性能方面二者并不相同 App类是在扫描spring-config时,一口气将所有的对象全部创建加载出来了的,属于饿汉模式,而BeanFact是按需加载bean的,下边代码中有使用bean的方法时,才会创建bean,我们以代码来观察
我们首先为User类加入了构造方法,当创建对象时就会提示
可以看到,我们都没有使用user对象,但是使用A方法直接会出现创建对象,B对象则什么都没有,那么我们加入user的方法再来看
可以发现,这时A,B类都调用了构造方法,也就是说B类更轻量
2)target文件夹
当我们修改了错误代码再次运行程序,可能源代码没有问题了,但是程序还是报错,我们这时不妨检查一下target文件夹下的内容,因为target文件夹下储存的是JVM真正运行的.class二进制文件,有时可能因为缓存问题,导致target目录下的.class文件没有及时更新,因此程序仍旧报错
3.使用Spring更简单的注册对象(使用注解)
前置工作:我们在使用更简单的方式时,需要在spring-config.xml(配置文件)中加入扫描对象的根路径
我们的base-package属性就是扫描bean对象的根路径,spring会自动扫描beans目录及其所有子目录,根据条件注入对象
1)使用五大类注解
我们的五大类注解有
1)@Controller【控制器】
2)@Service【服务】
3)@Repository【仓库】
4)@Configuration【配置】
5)@Component【组件】
我们在使用时只需要将注解加到要注册的目标类上边即可
加了五大类注解之后,spring在获取上下文对象时就会扫描刚刚设置的扫描根路径中的所有类,当类设置了五大类注解时,就会注册此类
1.五大类注解的区别和联系
区别:
在软件工程中,我们的代码分为四大层级
1.配置层(Configuration)
我们将程序运行所需的配置文件归属在该层,例如刚刚的spring-config.xml文件
2.控制层(Controller)
这一层负责检验前端所传过来的参数是否合法,像是安检一样,前端所传的参数会率先经过该层,如果参数有问题则会被返回并且提示异常
3.服务层(Service)
这一层负责数据组装和接口调用,例如我们需要使用哪些接口来处理前端传过来的参数,该返回怎样的数据给前端等
4.数据持久层(Repository/DAO)
这一层负责直接操作数据库,和数据库进行交互
仔细观察就会发现,我们的五大类注解和这几层的名字很相似,因此涉及五大类注解,每一类注解都对应了其中一层,第五大类@Component(组件)
则对应了一些不属于上述四大层级的其他类,作为程序的组件,我们在使用不同注解标记代码,就使得代码的可读性更强了,程序员可以直接判断当前类的作用
联系:
我们通过查看源码可以发现,前四个注解都是基于第五个注解实现的,也就是说前四个注解都是基于@Component,都是@Component的子类,所有的注解都属于程序的“组件”
2.spring使用五大类注解生成的beanName问题
在之前在spring-config.xml中添加bean标签实现对象注册时,我们的id属性对应了被注册对象的beanName,我们可以通过其访问到唯一的bean,但是使用五大类注解时我们没有显式的指定bean的beanName,那么我们如何访问非唯一的对象呢,我们这里可以查看spring的源码
查看该方法,发现“创建beanName方法(generateBeanName)”
可以发现其返回中调用了buildDefaultBeanName方法,查看该方法可以发现
其最后调用了decapitalize方法,那么这个方法就是最终答案了
可以发现,这个方法先执行了一个参数合法性的判断,之后第二个if中判断了传入的name长度是否大于一个字符,如果大于的话先查看这个name的第二个字符,看是否第二个字符是大写的,如果是,再检查name的第一个字符,如果第一个和第二个字符全是大写,那么返回的默认beanName就是这个类的name本身(传入的name),如果上述三个条件有一个不符合,那么就会将类名的首字母改为小写,作为beanName返回回去,因此我们上边才会说,beanName的默认命名方式为小驼峰,但是特殊情况就是前两个字符都是大写时beanName就是原类名
可以直接调用这个方法来查看结果,符合我们上述的分析
2)使用方法注解@Bean
@Bean方法只可以标记在类中的方法上,而不可以直接标记在类上,此注释表示将被标记方法返回的对象注册在spring当中,同时,使用方法注解时需要搭配一个五大类注解在方法所在的类上方,这样的目的是为了优化性能,因为我们的扫描路径下可能有的方法是非常多的,这时如果spring去扫描所有的方法看是否方法上查看了@Bean注解,这样效率会非常低,我们不妨为有方法注解的类添加五大类注解,达到缩小扫描范围的目的,优化性能,这个行为和添加扫描路径是一样的,不添加扫描路径的话扫描的就是整个java源代码目录,添加了扫描路径的话就只扫描目标路径及其子目录
根据分析我们可以得到一个name属性为zhangsan的user对象
可以发现确实如此
当然,我们想要访问@Bean方法注册的对象,也不一定非要使用方法名来访问,我们可以通过设置@Bean的属性来对beanName重命名(可以设置多个重命名,用包裹起来即可),但是当我们设置了这个属性后,我们便无法通过方法名的方式访问bean了
@Controller
public class setUser
@Bean(name = "user1","user2")
public User getUser()
User user = new User();
user.name = "zhangsan";
return user;
我们通过重命名的user1或者user2都可以获取到user对象了
4.使用Spring更简单的装配对象(对象注入)
顾名思义,就是使用spring更容易的将对象从spring中读取出来,之前我们要读取容器中的对象,需要先获取上下文对象,然后调用getBean方法才可以,现在我们只需要用注释的方法就可以轻松获取到了,我们可以用@Autowired注释和@Resource注释
1)属性注入(字段注入)
定义当前类的一个属性,属性类型就是要注入的对象类型,属性名采用小驼峰的方式,之后在该属性上添加@Autowired注释或者@Resource注释都可
@Controller
public class setUser2
@Autowired
private User user;
public void getName()
System.out.println(user.name);
可以发现我们可以提取到setUser2中的user的name属性,注入成功
2)构造方法注入(官方推荐)
定义一个属性,属性类型是要注入的对象,对象名为小驼峰,定义构造方法,像构造方法传入参数(要注入的对象),将属性对象的引用设置为传入的对象,在构造方法上添加@Autowired注解,而这里不能使用@Resource注解
@Controller
public class setUser3
private User user;
@Autowired
public setUser3(User user)
this.user = user;
public void getName()
System.out.println(user.name);
可以发现,我们仍然可以访问到向setUser3中注入的user对象的name属性
通过构造方法注入时,如果只写了一个构造方法,我们可以省略@Autowired注释,但是如果有多个构造方法,我们则不可以省略
3)Setter注入
定义私有属性,属性类型为要注入对象类型,属性名以小驼峰方式,通过设置私有属性的set方法来将对象注入到属性中,可以通过在set方法上添加@Autowired注解或@Resource注解
@Controller
public class setUser4
private User user;
@Autowired
public void setUser(User user)
this.user = user;
public void getName()
System.out.println(user.name);
拓展
1.属性注入,构造方法注入和Setter注入的区别?(三种注入方式的区别)
1)属性注入的写法最为简单,但是只用于IoC容器中,如果程序运行在非IoC容器环境下,很容易注入失败,通用性不强,引发问题
2)setter注入是曾经spring最推荐的注入方式,但是不同语言的set方法可能不同,甚至可能没有set方法,因此这种方法的通用性并不高
3)构造方法注入的通用性是最强的,因为几乎所有的语言的构造方法格式都是相同的,同时,构造方法会在构造类是率先调用,那么采用构造方法注入,可以第一时间将所需依赖注入到当前类当中完成初始化,再使用当前类时不会引发空指针异常,但是使用构造方法注入需要程序员自己检查代码不要有过多的参数,必须要符合单一设计原则
2.@Autowired 和 @Resource 的区别(两种注入方法的区别)
1)出身不同:@Resource来自于JDK官方 @Autowired来自于Spring
2)用法不同:@Resource不支持构造方法注入,仅支持其他两种,@Autowired支持三种注入方式
3)支持的参数不同:@Resource支持更多的参数设置,例如name,type等,而@Autowired仅支持require参数
3.解决@Bean注入多个对象的问题
@Controller
public class setUser
@Bean(name = "user1")
public User getUser()
User user = new User();
user.name = "zhangsan";
return user;
@Bean(name = "user2")
public User getUser2()
User user = new User();
user.name = "wangwu";
return user;
在上述代码中,我们写了两个getUser方法,都通过方法注解@Bean将两个方法返回的对象注册到了容器中
@Controller
public class NotSingle
@Autowired
private User user;
public void getName()
System.out.println(user.name);
我们在NotSingle类中注入user对象,再在main方法中创建这个实例调用getName方法看是否可以访问到user对象的name属性
可以发现触发了不是唯一对象的异常,这是因为我们之前注入了两个user对象,这时我们再注入user类到其他类中就不知道到底该注入user1,还是user2了,我们解决这个方法有三种方式
1.精确的描述要注入的bean的名称
例如上述代码,我们可以将属性名修改为user1或者user2,这样达到精确注入的目的
@Controller
public class NotSingle
@Autowired
private User user1;
public void getName()
System.out.println(user1.name);
运行后发现name为wangwu,符合user1的属性
2.通过设置@Resource注释的name属性来精确锁定某一对象
将@Resouce设置了name属性后,该类的私有属性(要注入的对象)的属性名可以达到给bean重命名的效果
@Controller
public class NotSingle
@Resource(name = "user1")
private User user;
public void getName()
System.out.println(user.name);
我们通过设置@Resource的name属性,锁定了要注入对象为user1,同时下边创建的user可以将user1对象在当前类中重命名,user和user1引用指向同一个user的地址,这时我们访问的结果仍然是zhangsan
3.使用 @Autowired注释和 @Qualifier搭配锁定某一对象
@Controller
public class NotSingle
@Autowired
@Qualifier("user2") //也可以是@Qualifier(value = "user2")
private User user;
public void getName()
System.out.println(user.name);
这时我们达到的效果和方法2相同,都锁定了user2对象并且将其重命名为user,执行效果为输出wangwu
5.Bean的作用域
1)案例
我们先来看一个案例
首先,创建一个Users类,设置属性name初始值为”暗裔剑魔“
@Controller
public class Users
public String name = "暗裔剑魔";
public void getName()
System.out.println(this.name);
创建一个类BeanLife,注入一个users对象,并且在自己的方法中新建一个users对象使其指向刚注入进来的users属性所指的对象,在方法中修改其名称为”时间刺客“
public class BeanLife
@Autowired
private Users users;
public void getNewName()
Users users_1 = users;
System.out.println("修改前原name" + user2.name);
users_1.name = "时间刺客";
再创建一个新的类,再注入users对象,打印users对象的名字
@Controller
public class BeanLife2
@Autowired
private Users users;
public void getName()
System.out.println(users.name);
按照我们一直以来的了解,注入的对象应该是在堆中开辟了新的空间,其作用域在类范围内,那么我们在类中再新建一个指向它的引用并且修改其属性,应该不会影响原本注册在spring中的users对象,也就是说新的类BeanLife2中读取道德users对象的name属性应该是”暗裔剑魔“,那么我们来看一下运行结果
我们可以发现,beanLife2得到的users对象的name属性竟然是”时间刺客“,
这是为什么呢?
这是因为Bean在spring中默认是单例模式的,也就是说无论在哪里创建新的对象,只要和bean对象类型beanName相同,那么这个新创建的对象也指向同一个内存地址
2)作用域类型
1.singleton(单例模式)(默认模式)
通常无状态的bean可以设置作用域为单例模式(无状态指后续代码没有改变bean的属性)
2.prototype 原型模式(多例模式)
即使后边修改,从另一个类中拿到的还是原本的spring bean:通常有状态的bean使用此模式,每一次请求都会创建新的实例,例如使用getBean方法获取时或者使用@Autowired注入时都会创建新的对象
当我们将案例中users的作用域设置为prototype时可以观察到
beanLife2取到的仍是原beans对象,没有被beanLife1修改
3.request 请求作用域 (Spring MVC)
每一次发送请求都会得到一个对象
4.session 会话作用域(Spring MVC)
每同一个session共用一个对象
5.application 全局作用域(Spring MVC)
所有用户共用一个对象,3,4,5是一个逐渐升级的过程 和singleton的应用场景不同 application应用于Spring Web作用域 singleton是Spring Core作用域 singleton作用于IoC的容器 而application作用于servlet容器
3)作用域设置
1.可以通过@Scope(“”)来设置,例如上边使用的
2.通过@Scope(ConfigurableBeanFactory.)的方式来设置,这样可以有效的避免我们用1方法传字符串时将单词拼错
6.Spring的执行流程
1.启动容器,通过main方法中ApplicationContext获取上下文对象同时启动容器
2.根据配置文件,通过xml中加入的bean对象,或者扫描目录,将带五大类注解或bean方法注解的对象注册入spring当中,注册时如果有类需要依赖别的类,例如A依赖B,那么在注册A类时会先将B类注入A的属性中,完成初始化 防止空指针异常,然后再注册A类
7.Bean的生命周期
1.实例化Bean:为Bean对象分配内存空间
2.设置属性:为Bean对象注入所需依赖
3.初始化:
1)执行各种通知
2)执行初始化的前置方法
3)执行Bean的构造方法:一种是执行@PostConstruct 另一种是执行init-method,一个是注解方式的初始化,一个是xml时代的,两个方法的优先级中,注解优先级要比init高
4)执行初始化的后置方法
4.使用bean
5.销毁bean
可以使用@PreDestroy 也可以重写DisposableBean接口方法 也可以执行 destroy-method 一个是注释产物,一个是方法,一个是xml产物
我们用代码来观察一下各个方法的执行顺序是否如上述所说一般
public class BeanLifeTest
@Autowired
private TestBean testBean;
@PostConstruct
public void PostConstruct()
System.out.println("执行了@PostConstruct方法!")Spring(万字详解版)