Spring
Spring简介
1.1历史
2002年,首次推出了spring框架的雏形:interface21框架
2004年3月24日,悉尼大学音乐学博士Rod Johnson首次推出Spring框架1.0,解决企业应用开发的复杂性。
设计理念:使现有的技术更加容易使用,本身是一套大杂烩,整合了现有的技术框架。
SSH:Struct2(视图框架) + Spring(融合剂) + Hibernate(全自动持久层框架)
SSM : SpringMvc(视图框架) + Spring(融合剂) + Mybatis(半自动持久层框架,可定制性更高)
1.2优点
Spring是一个开源的免费的框架(容器)
Spring是一个轻量级的、非入侵式的框架
控制反转(IOC),面向方面编程(AOP)
支持事务的处理,对框架整合的支持
总结一句话:Spring就是一个轻量级的控制反转(IoC)和面向方面编程(AOP)的框架!
1.3组成
1.4拓展
Spring Boot
- 一个快速开发的脚手架
- 基于Spring Boot可以快速的开发单个微服务
- 约定大于配置
Spring Cloud
- SpringCloud是基于SpringBoot实现的
现在大多数公司都在使用SpringBoot进行快速开发,所以先掌握Spring及SpringMVC很重要,承上启下!
IoC
假如我们需要在程序内构建一辆”车“,那么我们传统的思想一般是下面这样的。
想要构建一辆汽车,首先要依赖于车身,而车身又依赖于地盘。而底盘需要轮胎。最终我们得到下面的代码。
1 | public class Main { |
虽然这段代码的确能够满足需求,但是我们不难发现一个特点,就是它的耦合性非常高。假如我们需要更改轮胎的尺寸,我们需要将轮胎以及所有依赖轮胎的组件的构造方法全部更改一遍,加上size参数,例如下面那样。
1 | public class Main { |
而在实际开发过程中,需求变更是很常见的。而我们不难看出,以上程序的问题是:**当最底层代码改动之后,整个调⽤链上的所有代码都需要修改。**这在实际开发过程中就会严重拖慢我们的进度,那么有什么办法能够解决这个问题呢。我们看看以下的代码。
1 | public class Main { |
可以看到我们先创建了所有的下级依赖类,然后再通过参数传递的方式注入。这样我们不需 要在当前类中创建下级类了,所以下级类即使发⽣变化(创建或减少参数),当前类本身也⽆需修改任 何代码,这样就完成了程序的解耦。
以上就是所谓的控制反转式程序开发。此时⽆论底层类如何变化,整个调⽤链是不⽤做任何改变的,这样就完成了代码之间的解耦,从⽽实现了更加灵活、通⽤的程序设计了。
在传统的代码中对象创建顺序是:Car -> Framework -> Bottom -> Tire
改进之后解耦的代码的对象创建顺序是:Tire -> Bottom -> Framework -> Car
这里我们可以发现:通⽤程序的实现代码,类的创建顺序是反的,传统代码是 Car 控制并创建了 Framework,Framework 创建并创建了 Bottom,依次往下,⽽改进之后的控制权发⽣的反转,不再是上级对象创建并控制下级对象了,⽽是下级对象把注⼊将当前对象中,下级的控制权不再由上级类控制了,这样即使下级类发⽣任何改变,当前类都是不受影响的,这就是典型的控制反转,也就是 IoC 的实现思想。
仔细去思考一下 , 以前所有东西都是由程序去进行控制创建 , 而现在是由我们自行控制创建对象 , 把主动权交给了调用者 . 程序不用去管怎么创建,怎么实现了 . 它只负责提供一个接口 .
这种思想 , 从本质上解决了问题 , 我们不再去管理对象的创建了 , 更多的去关注业务的实现 . 耦合性大大降低 . 这也就是IOC的原型 !
IoC本质
控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法,也有人认为DI只是IoC的另一种说法。没有IoC的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,所谓控制反转就是:获得依赖对象的方式反转了。
IoC是Spring框架的核心内容,使用多种方式完美的实现了IoC,可以使用XML配置,也可以使用注解,新版本的Spring也可以零配置实现IoC。
Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从Ioc容器中取出需要的对象。
采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。
控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IoC容器,其实现方法是依赖注入(Dependency Injection,DI)。
IoC容器的使用
Bean注册与配置
1.Spring为我们提供了一个IoC容器用于存放我们需要使用的对象,我们可以将对象交给IoC容器来管理,当我们需要使用对象时,就可以向IoC容器索要,并由它来决定给我们哪一个对象。要使用IoC容器,就需要创建一个应用程序上下文,它代表的就是IoC容器,它会负责实例化、配置和组装Bean:
1 | ApplicationContext context = new ClassPathXmlApplicationContext("test.xml") |
它有很多种实现方式,这里使用xml配置文件所以使用ClassPathXmlApplicationContext。
2.当你写好了一个bean,可以在xml文件里添加上。
1 | <bean name="a" class="com.test.bean.Student"/> |
class指定bean的类型,name(或id)表示该bean的唯一标识。
我们可以给Bean起名字,也可以起别名,就像我们除了有一个名字之外,可能在家里还有自己的小名:
1 | <bean name="a" class="com.test.bean.Student"/> |
在xml文件上的bean可以通过context的getBean方法得到。getBean方法的参数可以是类型或name等。
3.那么现在又有新的问题了,IoC容器创建的Bean是只有一个还是每次索要的时候都会给我们一个新的对象?
实际上,我们配置的bean有两种模式(scope)。第一种是singleton
,默认情况下就是这一种,当然还有prototype
,表示为原型模式(为了方便叫多例模式也行)这种模式每次得到的对象都是一个新的。当Bean的作用域为单例模式时,那么它会在一开始(容器加载配置时)就被创建,我们之后拿到的都是这个对象。而处于原型模式下,只有在获取时才会被创建,也就是说,单例模式下,Bean会被IoC容器存储,只要容器没有被销毁,那么此对象将一直存在,而原型模式才是相当于在要用的时候直接new了一个对象,并不会被保存。
当然,如果我们希望单例模式下的Bean不用再一开始就加载,而是一样等到需要时再加载(加载后依然会被容器存储,之后一直使用这个对象了,不会再创建新的)我们也可以开启懒加载:
1 | <bean class="com.test.bean.Student" lazy-init="true"/> |
开启懒加载后,只有在真正第一次使用时才会创建对象。
因为单例模式下Bean是由IoC容器加载,但是加载顺序我们并不清楚,如果我们需要维护Bean的加载顺序(比如某个Bean必须要在另一个Bean之前创建)那么我们可以使用depends-on
来设定前置加载Bean,这样被依赖的Bean一定会在之前加载,比如Teacher应该在Student之前加载:
1 | <bean name="teacher" class="com.test.bean.Teacher"/> |
这样就可以保证Bean的加载顺序了。
依赖注入
4.IoC容器在创建对象时,需要将我们预先给定的属性注入到对象中,非常简单,我们可以使用property
标签来实现,
首先依赖注入要求对应的属性必须有一个set方法:
1 | public class Student { |
然后我们将bean标签展开插入property标签:
1 | <bean name="teacher" class="com.test.bean.ProgramTeacher"/> |
name="teacher"
:指定了Student
类中需要注入的属性名称,即teacher
属性。ref="teacher"
:表示这个teacher
属性的值引用了前面定义的teacher
Bean。也就是说,Student
类的teacher
属性会被注入为ProgramTeacher
类的实例。
更改这两个属性,就达到了切换不同的实现。
当然,依赖注入并不一定要注入其他的Bean,也可以是一个简单的值:
1 | <bean name="student" class="com.test.bean.Student"> |
直接使用value
可以直接传入一个具体值。
5.实际上,在很多情况下,类中的某些参数是在构造方法中就已经完成的初始化,而不是创建之后,比如:
1 | public class Student { |
我们前面说了,Bean实际上是由IoC容器进行创建的,但是现在我们修改了默认的无参构造,可以看到配置文件里面报错了:
指定构造器
很明显,是因为我们修改了构造方法,IoC容器默认只会调用无参构造,所以,我们需要指明一个可以用的构造方法,我们展开bean标签,添加一个constructor-arg
标签:
1 | <bean name="teacher" class="com.test.bean.ArtTeacher"/> |
这里的constructor-arg
就是构造方法的一个参数,这个参数可以写很多个,会自动匹配符合里面参数数量的构造方法,这里匹配的就是我们刚刚编写的需要一个参数的构造方法。
通过这种方式,我们也能实现依赖注入,只不过现在我们将依赖注入的时机提前到了对象构造时。
那要是出现这种情况呢?现在我们的Student类中是这样定义的:
1 | public class Student { |
此时我们希望使用的是二号构造方法,那么怎么才能指定呢?有2种方式,我们可以给标签添加类型:
1 | <constructor-arg value="1" type="int"/> |
也可以指定为对应的参数名称:
1 | <constructor-arg value="1" name="age"/> |
反正只要能够保证我们指定的参数匹配到目标构造方法即可。
6.特殊的类型
现在我们的类中出现了一个比较特殊的类型,它是一个集合类型:
1 | public class Student { |
对于这种集合类型,有着特殊的支持:
1 | <bean name="student" class="com.test.bean.Student"> |
不仅仅是List,Map、Set这类常用集合类包括数组在内,都是支持这样编写的,比如Map类型,我们也可以使用entry
来注入:
1 | <bean name="student" class="com.test.bean.Student"> |
至此,我们就已经完成了两种依赖注入的学习:
- Setter依赖注入:通过成员属性对应的set方法完成注入。
- 构造方法依赖注入:通过构造方法完成注入。
自动装配
autowire实现自动装配
在之前,如果我们需要使用依赖注入的话,我们需要对property
参数进行配置:
1 | <bean name="student" class="com.test.bean.Student"> |
但是有些时候为了方便,我们也可以开启自动装配。自动装配就是让IoC容器自己去寻找需要填入的值,我们只需要将set方法提供好就可以了,这里需要添加autowire属性:
1 | <bean name="student" class="com.test.bean.Student" autowire="byType"/> |
autowire
属性有两个值普通,一个是byName,还有一个是byType,顾名思义,一个是根据类型去寻找合适的Bean自动装配,还有一个是根据名字去找,这样我们就不需要显式指定property
了。
此时set方法旁边会出现一个自动装配图标,效果和上面是一样的。
对于使用构造方法完成的依赖注入,也支持自动装配,我们只需要将autowire修改为:
1 | <bean name="student" class="com.test.bean.Student" autowire="constructor"/> |
这样,我们只需要提供一个对应参数的构造方法就可以了(这种情况默认也是byType寻找的):
这样同样可以完成自动注入:
autowire-candidate和primary解决类型相同问题
自动化的东西虽然省事,但是太过机械,有些时候,自动装配可能会遇到一些问题,比如出现了下面的情况:
此时,由于autowire
的规则为byType,存在两个候选Bean,但是我们其实希望ProgramTeacher这个Bean在任何情况下都不参与到自动装配中,此时我们就可以将它的自动装配候选关闭:
1 | <bean name="teacher" class="com.test.bean.ArtTeacher"/> |
当autowire-candidate
设定false时,这个Bean将不再作为自动装配的候选Bean,此时自动装配候选就只剩下一个唯一的Bean了,报错消失,程序可以正常运行。
除了这种方式,我们也可以设定primary属性,表示这个Bean作为主要的Bean,当出现歧义时,也会优先选择:
1 | <bean name="teacher" class="com.test.bean.ArtTeacher" primary="true"/> |
这样写程序依然可以正常运行,并且选择的也是ArtTeacher。
生命周期与继承
init-method
和destroy-method
来指定初始和销毁方法
除了修改构造方法,我们也可以为Bean指定初始化方法inti()和销毁方法destroy(),以便在对象创建和被销毁时执行一些其他的任务。
我们可以通过init-method
和destroy-method
来指定:
1 | <bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy"/> |
那么什么时候是初始化,什么时候又是销毁呢?
1 | //当容器创建时,默认情况下Bean都是单例的,那么都会在一开始就加载好,对象构造完成后,会执行init-method |
所以说,最后的结果为:
注意,如果Bean不是单例模式,而是采用的原型模式,那么就只会在获取时才创建,并调用init-method,而对应的销毁方法不会被调用(因此,对于原型模式下的Bean,Spring无法顾及其完整生命周期,而在单例模式下,Spring能够从Bean对象的创建一直管理到对象的销毁)。
parent实现属性继承
Bean之间也是具备继承关系的,只不过这里的继承并不是类的继承,而是属性的继承,比如:
1 | public class SportStudent { |
1 | public class ArtStudent { |
此时,我们先将ArtStudent注册一个Bean:
1 | <bean name="artStudent" class="com.test.bean.ArtStudent"> |
这里我们会注入一个name的初始值,此时我们创建了一个SportStudent的Bean,我们希望这个Bean的属性跟刚刚创建的Bean属性是一样的,那么我们可以写一个一模一样的:
1 | <bean class="com.test.bean.SportStudent"> |
但是如果属性太多的话,是不是写起来有点麻烦?这种情况,我们就可以配置Bean之间的继承关系了,我们可以让SportStudent这个Bean直接继承ArtStudent这个Bean配置的属性:
1 | <bean class="com.test.bean.SportStudent" parent="artStudent"/> |
这样,在ArtStudent Bean中配置的属性,会直接继承给SportStudent Bean(注意,所有配置的属性,在子Bean中必须也要存在, 并且可以进行注入,否则会出现错误)当然,如果子类中某些属性比较特殊,也可以在继承的基础上单独配置:
1 | <bean name="artStudent" class="com.test.bean.ArtStudent" abstract="true"> |
abstract将Bean仅作为一个配置模版
如果我们只是希望某一个Bean仅作为一个配置模版供其他Bean继承使用,那么我们可以将其配置为abstract,这样,容器就不会创建这个Bean的对象了:
1 | <bean name="artStudent" class="com.test.bean.ArtStudent" abstract="true"> |
注意,一旦声明为抽象Bean,那么就无法通过容器获取到其实例化对象了。
大标签beans实现全局默认配置
如果我们希望整个上下文中所有的Bean都采用某种配置,我们可以在最外层的beans标签中进行默认配置:
这样,即使Bean没有配置某项属性,但是只要在最外层编写了默认配置,那么同样会生效,除非Bean自己进行配置覆盖掉默认配置。
工厂模式和工厂Bean
factory-method实现static方法下的工厂模式创建工厂Bean
前面我们介绍了IoC容器的Bean创建机制,默认情况下,容器会调用Bean对应类型的构造方法进行对象创建,但是在某些时候,我们可能不希望外界使用类的构造方法完成对象创建,比如在工厂方法设计模式中(详情请观看《Java设计模式》篇 视频教程)我们更希望 Spring不要直接利用反射机制通过构造方法创建Bean对象, 而是利用反射机制先找到对应的工厂类,然后利用工厂类去生成需要的Bean对象:
1 | public class Student { |
1 | public class StudentFactory { |
此时Student有一个工厂,我们正常情况下需要使用工厂才可以得到Student对象,现在我们希望Spring也这样做,不要直接去反射搞构造方法创建,我们可以通过factory-method进行指定:
1 | <bean class="com.test.bean.StudentFactory" factory-method="getStudent"/> |
注意,这里的Bean类型需要填写为Student类的工厂类,并且添加factory-method指定对应的工厂方法,但是最后注册的是工厂方法的返回类型,所以说依然是Student的Bean:
此时我们再去进行获取,拿到的也是通过工厂方法得到的对象:
这里有一个误区,千万不要认为是我们注册了StudentFactory这个Bean,class填写为这个类这个只是为了告诉Spring我们的工厂方法在哪个位置,真正注册的是工厂方法提供的东西。
可以发现,当我们采用工厂模式后,我们就无法再通过配置文件对Bean进行依赖注入等操作了,而是只能在工厂方法中完成,这似乎与Spring的设计理念背道而驰?
两种方式实现非static方法的工厂模式创建工厂Bean
第一种
当然,可能某些工厂类需要构造出对象之后才能使用,我们也可以将某个工厂类直接注册为工厂Bean:
1 | public class StudentFactory { |
现在需要StudentFactory对象才可以获取到Student,此时我们就只能先将其注册为Bean了:
1 | <bean name="studentFactory" class="com.test.bean.StudentFactory"/> |
像这样将工厂类注册为Bean,我们称其为工厂Bean,然后再使用factory-bean
来指定Bean的工厂Bean:
1 | <bean factory-bean="studentFactory" factory-method="getStudent"/> |
注意,使用factory-bean之后,不再要求指定class,我们可以直接使用了:
此时可以看到,工厂方法上同样有了图标,这种方式,由于工厂类被注册为Bean,此时我们就可以在配置文件中为工厂Bean配置依赖注入等内容了。
第二种
通过在bean工厂类实现FactoryBean< T > 接口,T为该工厂生产Bean的类型。它有两个需要实现的方法,一个是获取工厂Bean的方法,一个是生产Bean的类型。
这里还有一个很细节的操作,如果我们想获取工厂Bean为我们提供的Bean,可以直接输入工厂Bean的名称,这样不会得到工厂Bean的实例,而是工厂Bean生产的Bean的实例:
1 | Student bean = (Student) context.getBean("studentFactory"); |
当然,如果我们需要获取工厂类的实例,可以在名称前面添加&
符号:
1 | StudentFactory bean = (StudentFactory) context.getBean("&studentFactory"); |
又是一个小细节。
使用注解开发
既然现在要使用注解来进行开发,那么我们就删掉之前的xml配置文件吧,我们来看看使用注解能有多方便。
1 | ApplicationContext context = new AnnotationConfigApplicationContext(); |
现在我们使用AnnotationConfigApplicationContext作为上下文实现,它是注解配置的。
既然现在采用注解,我们就需要使用类来编写配置文件,在之前,我们如果要编写一个配置的话,需要:
1 |
|
现在我们只需要创建一个配置类(类上加上configuration注解)就可以了:
1 |
|
这两者是等价的,同样的,在一开始会提示我们没有配置上下文(新版idea可能自动配置好了):
这里按照要求配置一下就可以,同上,这个只是会影响IDEA的代码提示,不会影响程序运行。
我们可以为AnnotationConfigApplicationContext指定一个默认的配置类:
1 | ApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class); |
那么现在我们该如何配置Bean呢?
1 |
|
这样写相对于配置文件中的:
1 |
|
通过@Import还可以引入其他配置类:
1 | //在讲解到Spring原理时,我们还会遇到它,目前只做了解即可。 |
只不过现在变成了由Java代码为我们提供Bean配置,这样会更加的灵活,也更加便于控制Bean对象的创建。
1 | ApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class); |
使用方法是相同的,这跟使用XML配置是一样的。
那么肯定就有小伙伴好奇了,我们之前使用的那么多特性在哪里配置呢?首先,初始化方法和摧毁方法、自动装配可以直接在@Bean注解中进行配置:
1 |
|
其次,我们可以使用一些其他的注解来配置其他属性,比如:
1 |
|
对于那些我们需要通过构造方法或是Setter完成依赖注入的Bean,比如:
1 | <bean name="teacher" class="com.test.bean.ProgramTeacher"/> |
像这种需要引入其他Bean进行的注入,我们可以直接将其作为形式参数放到方法中:
1 |
|
此时我们可以看到,旁边已经出现图标了:
运行程序之后,我们发现,这样确实可以直接得到对应的Bean并使用。
只不过,除了这种基于构造器或是Setter的依赖注入之外,我们也可以直接到Bean对应的类中使用自动装配:
1 | public class Student { |
现在,我们甚至连构造方法和Setter都不需要去编写了,就能直接完成自动装配,太棒了。
当然,@Autowired并不是只能用于字段,对于构造方法或是Setter,它同样可以:
1 | public class Student { |
@Autowired默认采用byType的方式进行自动装配,也就是说会使用类型进行装配,那么要是出现了多个相同类型的Bean,如果我们想要指定使用其中的某一个该怎么办呢?
1 |
|
此时,我们可以配合@Qualifier进行名称匹配:
1 | public class Student { |
随着Java版本的更新迭代,某些javax包下的包,会被逐渐弃用并移除。在JDK11版本以后,javax.annotation这个包被移除并且更名为jakarta.annotation其中有一个非常重要的注解,叫做@Resource,它的作用与@Autowired时相同的,也可以实现自动装配,但是在IDEA中并不推荐使用@Autowired注解对成员字段进行自动装配,而是推荐使用@Resource,如果需要使用这个注解,还需要额外导入包:
1 | <dependency> |
使用方法一样,直接替换掉就可以了:
1 | public class Student { |
只不过,他们两有些机制上的不同:
- @Resource默认ByName如果找不到则ByType,可以添加到set方法、字段上。
- @Autowired默认是byType,只会根据类型寻找,可以添加在构造方法、set方法、字段、方法参数上。
因为@Resource的匹配机制更加合理高效,因此idea官方并不推荐使用@Autowired字段注入,当然,实际上Spring官方更推荐我们使用基于构造方法或是Setter的@Autowired注入,比如Setter 注入的一个好处是,Setter 方法使该类的对象能够在以后重新配置或重新注入。其实,最后使用哪个注解,还是看你自己,要是有强迫症不能忍受黄标但是又实在想用字段注入,那就用@Resource注解。
除了这个注解之外,还有@PostConstruct和@PreDestroy,它们效果和init-method和destroy-method是一样的:
1 |
|
我们只需要将其添加到对应的方法上即可:
1 | AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class); |
可以看到效果是完全一样的,这些注解都是jakarta.annotation提供的。
前面我们介绍了使用@Bean来注册Bean,但是实际上我们发现,如果只是简单将一个类作为Bean的话,这样写还是不太方便,因为都是固定模式,就是单纯的new一个对象出来,能不能像之前一样,让容器自己反射获取构造方法去生成这个对象呢?
肯定是可以的,我们可以在需要注册为Bean的类上添加@Component
注解来将一个类进行注册**(现在最常用的方式)**,不过要实现这样的方式,我们需要添加一个自动扫描来告诉Spring,它需要在哪些包中查找我们提供的@Component
声明的Bean。
1 | //同样可以自己起名字 |
要注册这个类的Bean,只需要添加@Component即可,然后配置一下包扫描:
1 |
|
Spring在扫描对应包下所有的类时,会自动将那些添加了@Component的类注册为Bean,是不是感觉很方便?只不过这种方式只适用于我们自己编写类的情况,如果是第三方包提供的类,只能使用前者完成注册,并且这种方式并不是那么的灵活。
不过,无论是通过@Bean还是@Component形式注册的Bean,Spring都会为其添加一个默认的name属性,比如:
1 |
|
它的默认名称生产规则依然是类名并按照首字母小写的驼峰命名法来的,所以说对应的就是student:
1 | Student student = (Student) context.getBean("student"); //这样同样可以获取到 |
同样的,如果是通过@Bean注册的,默认名称是对应的方法名称:
1 |
|
1 | Student student = (Student) context.getBean("artStudent"); |
相比传统的XML配置方式,注解形式的配置确实能够减少我们很多工作量。并且,对于这种使用@Component
注册的Bean,如果其构造方法不是默认无参构造,那么默认会对其每一个参数都进行自动注入:
1 |
|
最后,对于我们之前使用的工厂模式,Spring也提供了接口,我们可以直接实现接口表示这个Bean是一个工厂Bean:
1 |
|
实际上跟我们之前在配置文件中编写是一样的,这里就不多说了。
请注意,使用注解虽然可以省事很多,代码也能变得更简洁,但是这并不代表XML配置文件就是没有意义的,它们有着各自的优点,在不同的场景下合理使用,能够起到事半功倍的效果。
至此,关于Spring的IoC基础部分,我们就全部介绍完了。在最后,完成一个问题,现在有两个类:
1 |
|
1 |
|
这两个类互相需要注入对方的实例对象,这个时候Spring会怎么进行处理呢?如果Bean变成原型模式,Spring又会怎么处理呢?
Spring高级特性
Bean Aware
在Spring中提供了一些以Aware结尾的接口,实现了Aware接口的bean在被初始化之后,可以获取相应资源。Aware的中文意思为感知。简单来说,他就是一个标识,实现此接口的类会获得某些感知能力,Spring容器会在Bean被加载时,根据类实现的感知接口,会调用类中实现的对应感知方法。
比如BeanNameAware之类的以Aware结尾的接口,这个接口获取的资源就是BeanName:
1 |
|
实现BeanClassLoaderAware接口,那么它能够使得我们可以在Bean加载阶段就获取到当前Bean的类加载器:
1 |
|
实现ApplicationContextAware接口,它可以使我们在Bean加载阶段得到Bean的应用程序上下文。
1 |
|
任务调度
为了执行某些任务,我们可能需要一些非常规的操作,比如我们希望使用多线程来处理我们的结果或是执行一些定时任务,到达指定时间再去执行。这时我们首先想到的就是创建一个新的线程来处理,或是使用TimerTask来完成定时任务,但是我们有了Spring框架之后,就不用这样了,因为Spring框架为我们提供了更加便捷的方式进行任务调度。
异步任务
首先我们来看异步任务执行,需要使用Spring异步任务支持,我们需要1.在配置类上添加@EnableAsync
注解。
1 |
|
接着我们只需要2.在需要异步执行的方法上,添加@Async
注解即可将此方法标记为异步,当此方法被调用时,会异步执行,也就是新开一个线程执行,而不是在当前线程执行。我们来测试一下:
1 |
|
现在我们在主方法中分别调用一下试试看:
1 | public static void main(String[] args) throws InterruptedException { |
可以看到,我们的任务执行结果为:
很明显,异步执行的任务并不是在当前线程启动的,而是在其他线程启动的,所以说并不会在当前线程阻塞,可以看到马上就开始执行下一行代码,调用同步执行的任务了。
因此,当我们要将Bean的某个方法设计为异步执行时,就可以直接添加这个注解。但是需要注意,添加此注解要求方法的返回值只能是void或是Future类型才可以。
还有,在使用时,可能还会出现这样的信息:
虽然出现了这样的信息,但是我们的程序依然可以正常运行,这是因为Spring默认会从容器中选择一个Executor
类型的实例,并使用它来创建线程执行任务,这是Spring推荐的方式,当然,如果没有找到,那么会使用自带的 SimpleAsyncTaskExecutor 处理异步方法调用。
肯定会有小伙伴疑惑,什么情况?!这个方法很明显我们并没有去编写异步执行的逻辑,那么为什么会异步执行呢?这里很明显是同步调用的方法啊。的确,如果这个Bean只是一个简简单单的Student类型的对象,确实做不到。但是它真的只是一个简简单单的Student类型对象吗?
1 | Student student = context.getBean(Student.class); |
我们来看看结果:
???这是什么东西?这实际上Spring帮助我们动态生成的一个代理类,我们原本的类代码已经被修改了,能做到这样的操作,这其实都是AOP的功劳。
定时任务
看完了异步任务,我们接着来看定时任务,定时任务其实就是指定在哪个时候再去执行,在JavaSE阶段我们使用过TimerTask来执行定时任务。Spring中的定时任务是全局性质的,当我们的Spring程序启动后,那么定时任务也就跟着启动了,我们可以1.在配置类上添加@EnableScheduling
注解:
1 |
|
接着我们可以直接2.在配置类里面编写定时任务,把我们要做的任务写成方法,并添加@Scheduled
注解:
1 | //单位依然是毫秒,这里是每两秒钟打印一次 |
我们注意到@Scheduled
中有很多参数,我们需要指定’cron’, ‘fixedDelay(String)’, or ‘fixedRate(String)’的其中一个,否则无法创建定时任务,他们的区别如下:
- fixedDelay:在上一次定时任务执行完之后,间隔多久继续执行。
- fixedRate:无论上一次定时任务有没有执行完成,两次任务之间的时间间隔。
- cron:如果嫌上面两个不够灵活,你还可以使用cron表达式来指定任务计划。
有关cron表达式:https://blog.csdn.net/sunnyzyq/article/details/98597252
监听器
监听实际上就是等待某个事件的触发,当事件触发时,对应事件的监听器就会被通知,简单介绍一下:
1 |
|
要编写监听器,我们只需要1.让Bean继承ApplicationListener就可以了,并且2.将类型指定为对应的Event事件,这样,当发生某个事件时就会通知我们,比如ContextRefreshedEvent,这个事件会在Spring容器初始化完成会触发一次:
是不是感觉挺智能的?Spring内部有各种各样的事件,当然我们也可以自己编写事件,然后在某个时刻发布这个事件到所有的监听器:
1 | public class TestEvent extends ApplicationEvent { //自定义事件需要继承ApplicationEvent |
1 |
|
比如现在我们希望在定时任务中每秒钟发生一次这个事件:
1 |
|
此时,发布事件旁边出现了图标,说明就可以了:
我们可以点击这个图标快速跳转到哪里监听了这个事件。我们来看看运行结果吧:
这样,我们就实现了自定义事件发布和监听。
SpringEL表达式
SpEL 是一种强大,简洁的装配 Bean 的方式,用于动态地访问和操作对象的属性、调用方法、执行运算等,它可以通过运行期间执行的表达式将值装配到我们的属性或构造函数当中,更可以调用 JDK 中提供的静态常量,获取外部 Properties 文件中的的配置。
外部属性注入
有些时候,我们甚至可以将一些外部配置文件中的配置进行读取,并完成注入。
我们需要创建以.properties
结尾的配置文件,这种配置文件格式很简单,类似于Map,需要一个Key和一个Value,中间使用等号进行连接,这里我们在resource目录下创建一个test.properties
文件:
1 | test.name=只因 |
这样,Key就是test.name
,Value就是只因
,我们可以通过一个注解直接读取到外部配置文件中对应的属性值,首先我们需要引入这个配置文件,我们可以在1.配置类上添加@PropertySource
注解:
1 |
|
接着,我们就可以开始快乐的使用了,我们可以使用2. @Value 注解将外部配置文件中的值注入到任何我们想要的位置,就像我们之前使用@Resource自动注入一样:
1 |
|
@Value
中的${...}
表示占位符,它会读取外部配置文件的属性值装配到属性中,如果配置正确没问题的话,这里甚至还会直接显示对应配置项的值:
我们来测试一下吧:
如果遇到乱码的情况,请将配置文件的编码格式切换成UTF-8(可以在IDEA设置中进行配置)然后在@PropertySource注解中添加属性 encoding = “UTF-8” 这样就正常了,当然,其实一般情况下也很少会在配置文件中用到中文。
除了在字段上进行注入之外,我们3.也可以在需要注入的方法中使用:
1 |
|
当然,如果我们只是想简单的注入一个常量值,4.也可以直接填入固定值:
1 | private final String name; |
当然,@Value 的功能还远不止这些,配合SpringEL表达式,能够实现更加强大的功能。
SpEL简单使用
首先我们来看看如何创建一个SpEL表达式:
1 | ExpressionParser parser = new SpelExpressionParser(); |
这里得到的就是一个很简单的 Hello World 字符串,字符串使用单引号囊括,SpEL是具有运算能力的。
我们可以像写Java一样,对这个字符串进行各种操作,比如调用方法之类的:
1 | Expression exp = parser.parseExpression("'Hello World'.toUpperCase()"); //调用String的toUpperCase方法 |
不仅能调用方法、还可以访问属性、使用构造方法等。
对于Getter方法,我们可以像访问属性一样去使用:
1 | //比如 String.getBytes() 方法,就是一个Getter,那么可以写成 bytes |
表达式可以不止一级,我们可以多级调用:
1 | Expression exp = parser.parseExpression("'Hello World'.bytes.length"); //继续访问数组的length属性 |
我们继续来试试看构造方法,其实就是写Java代码,只是可以写成这种表达式而已:
1 | Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); |
它甚至还支持根据特定表达式,从给定对象中获取属性出来:
1 |
|
1 | Student student = context.getBean(Student.class); |
拿到对象属性之后,甚至还可以继续去处理:
1 | Expression exp = parser.parseExpression("name.bytes.length"); //拿到name之后继续getBytes然后length |
除了获取,我们也可以调用表达式的setValue方法来设定属性的值:
1 | Expression exp = parser.parseExpression("name"); |
除了属性调用,我们也可以使用运算符进行各种高级运算:
1 | Expression exp = parser.parseExpression("66 > 77"); //比较运算 |
1 | Expression exp = parser.parseExpression("99 + 99 * 3"); //算数运算 |
对于那些需要导入才能使用的类,我们需要使用一个特殊的语法:
1 | Expression exp = parser.parseExpression("T(java.lang.Math).random()"); //由T()囊括,包含完整包名+类名 |
集合操作相关语法
现在我们的类中存在一些集合类:
1 |
|
我们可以使用SpEL快速取出集合中的元素:
1 | Expression exp = parser.parseExpression("map['test']"); //对于Map这里映射型,可以直接使用map[key]来取出value |
1 | Expression exp = parser.parseExpression("list[2]"); //对于List、数组这类,可以直接使用[index] |
我们也可以快速创建集合:
1 | Expression exp = parser.parseExpression("{5, 2, 1, 4, 6, 7, 0, 3, 9, 8}"); //使用{}来快速创建List集合 |
1 | Expression exp = parser.parseExpression("{{1, 2}, {3, 4}}"); //它是支持嵌套使用的 |
1 | //创建Map也很简单,只需要key:value就可以了,怎么有股JSON味 |
你以为就这么简单吗,我们还可以直接根据条件获取集合中的元素:
1 |
|
1 | Expression exp = parser.parseExpression("list.?[score > 3]"); //选择学分大于3分的科目 |
我们还可以针对某个属性创建对应的投影集合:
1 | Expression exp = parser.parseExpression("list.![name]"); //使用.!创建投影集合,这里创建的时课程名称组成的新集合 |
我们接着来介绍安全导航运算符,安全导航运算符用于避免NullPointerException,它来自Groovy语言。通常,当您有对对象的引用时,您可能需要在访问对象的方法或属性之前验证它是否为空。为了避免这种情况,安全导航运算符返回null而不是抛出异常。以下示例显示了如何使用安全导航运算符:
1 | Expression exp = parser.parseExpression("name.toUpperCase()"); //如果Student对象中的name属性为null |
当遇到null时很不方便,我们还得写判断:
1 | if(student.name != null) |
Java 8之后能这样写:
1 | Optional.ofNullable(student.name).ifPresent(System.out::println); |
但是你如果写过Kotlin:
1 | println(student.name?.toUpperCase()); |
类似于这种判空问题,我们就可以直接使用安全导航运算符,SpEL也支持这种写法:
1 | Expression exp = parser.parseExpression("name?.toUpperCase()"); |
当遇到空时,只会得到一个null,而不是直接抛出一个异常:
我们可以将SpEL配合 @Value 注解或是xml配置文件中的value属性使用,比如XML中可以这样写:
1 | <bean id="numberGuess" class="org.spring.samples.NumberGuess"> |
或是使用注解开发:
1 | public class FieldValueTestBean { |
这样,我们有时候在使用配置文件中的值时,就能进行一些简单的处理了。
有关更多详细语法教程,请前往:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-language-ref
AOP面向切片
AOP(Aspect Oriented Programming)思想实际上就是:在运行时,动态地将代码切入到类的指定方法、指定位置上。也就是说,我们可以使用AOP来帮助我们在方法执行前或执行之后,做一些额外的操作,实际上,它就是代理!
通过AOP我们可以在保证原有业务不变的情况下,添加额外的动作,比如我们的某些方法执行完成之后,需要打印日志,那么这个时候,我们就可以使用AOP来帮助我们完成,它可以批量地为这些方法添加动作。可以说,它相当于将我们原有的方法,在不改变源代码的基础上进行了增强处理。
相当于我们的整个业务流程,被直接斩断,并在断掉的位置添加了一个额外的操作,再连接起来,也就是在一个切点位置插入内容。它的原理实际上就是通过动态代理机制实现的,我们在JavaWeb阶段已经给大家讲解过动态代理了。不过Spring底层并不是使用的JDK提供的动态代理,而是使用的第三方库实现,它能够以父类的形式代理,而不仅仅是接口。
使用配置实现AOP
在开始之前,我们先换回之前的XML配置模式,注意这里我们还加入了一些新的AOP相关的约束进来,建议直接CV下面的:
1 |
|
Spring是支持AOP编程的框架之一(实际上它整合了AspectJ框架的一部分),要使用AOP我们需要先导入一个依赖:
1 | <dependency> |
那么,如何使用AOP呢?首先我们要明确,要实现AOP操作,我们需要知道这些内容:
- 需要切入的类,类的哪个方法需要被切入(切到哪,找到需要切入的bean)
- 切入之后需要执行什么动作(切了干啥,新建切入类)
- 是在方法执行前切入还是在方法执行后切入(切入时机)
- 如何告诉Spring需要进行切入(怎么切)
比如现在我们希望对这个学生对象的study
方法进行增强,在不修改源代码的情况下,增加一些额外的操作:
1 | public class Student { |
1 | <bean class="org.example.entity.Student"/> |
那么我们按照上面的流程,依次来看,首先需要解决的问题是,找到需要切入的类,很明显,就是这个Student类,我们要切入的是这个study
方法。
第二步,我们切入之后要做什么呢?这里我们直接创建一个新的类,并将要执行的操作写成一个方法:
1 | public class StudentAOP { |
注意这个类也得注册为Bean才可以:
1 | <bean id="studentAOP" class="org.example.entity.StudentAOP"/> |
第三步,我们要明确这是在方法执行之前切入还是执行之后切入,很明显,按照上面的要求,我们需要执行之后进行切入。
第四步,最关键的来了,我们怎么才能告诉Spring我们要进行切入操作呢?这里我们需要在配置文件中进行AOP配置:
1 | <aop:config> |
接着我们需要添加一个新的切点,首先填写ID,这个随便起都可以:
1 | <aop:pointcut id="test" expression=""/> |
然后就是通过后面的expression
表达式来选择到我们需要切入的方法,这个表达式支持很多种方式进行选择,Spring AOP支持以下AspectJ切点指示器(PCD)用于表达式:
execution
:用于匹配方法执行连接点。这是使用Spring AOP时使用的主要点切割指示器。within
:限制匹配到某些类型的连接点(使用Spring AOP时在匹配类型中声明的方法的执行)。this
:限制与连接点匹配(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。target
:限制匹配连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。args
:限制与连接点匹配(使用Spring AOP时方法的执行),其中参数是给定类型的实例。@target
:限制匹配连接点(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注释。@args
:限制匹配到连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。@within
:限制与具有给定注释的类型中的连接点匹配(使用Spring AOP时在带有给定注释的类型中声明的方法的执行)。@annotation
:与连接点主体(在Spring AOP中运行的方法)具有给定注释的连接点匹配的限制。
其中,我们主要学习的execution
填写格式如下:
1 | 修饰符 包名.类名.方法名称(方法参数) |
- 修饰符:public、protected、private、包括返回值类型、static等等(使用*代表任意修饰符)
- 包名:如com.test(* 代表全部,比如com.*代表com包下的全部包)
- 类名:使用*也可以代表包下的所有类
- 方法名称:可以使用*代表全部方法
- 方法参数:填写对应的参数即可,比如(String, String),也可以使用*来代表任意一个参数,使用..代表所有参数。
也可以使用其他属性来进行匹配,比如@annotation
可以用于表示标记了哪些注解的方法被切入,这里我们就只是简单的执行,所以说只需要这样写就可以了:
1 | <aop:pointcut id="test" expression="execution(* org.example.entity.Student.study())"/> |
这样,我们就指明了需要切入的方法,然后就是将我们的增强方法,我们在里面继续添加aop:aspect
标签,并使用ref
属性将其指向我们刚刚注册的AOP类Bean:
1 | <aop:config> |
接着就是添加后续动作了,当然,官方支持的有多种多样的,比如执行前、执行后、抛出异常后、方法返回后等等:
其中around方法为环绕方法,自定义度会更高,我们会在稍后介绍。这里我们按照上面的要求,直接添加后续动作,注意需要指明生效的切点:
1 | <aop:aspect ref="studentAOP"> |
这样,我们就成功配置好了,配置正确会在旁边出现图标:
我们来试试看吧:
1 | public static void main(String[] args) { |
结果如下:
可以看到在我们原本的方法执行完成之后,它还继续执行了我们的增强方法,这实际上就是动态代理做到的,实现在不修改原有代码的基础上,对方法的调用进行各种增强,在之后的SpringMVC学习中,我们甚至可以使用它来快速配置访问日志打印。
前面我们说了,AOP是基于动态代理实现的,所以说我们如果直接获取Bean的类型,会发现不是原本的类型了:
1 | Student bean = context.getBean(Student.class); |
这里其实是Spring通过CGLib为我们生成的动态代理类,也就不难理解为什么调用方法会直接得到增强之后的结果了。包括我们前面讲解Spring的异步任务调度时,为什么能够直接实现异步,其实就是利用了AOP机制实现的方法增强。
虽然这些功能已经非常强大了,但是仅仅只能简单的切入还是不能满足一些需求,在某些情况下,我们可以需求方法执行的一些参数,比如方法执行之后返回了什么,或是方法开始之前传入了什么参数等等,现在我们修改一下Student中study
方法的参数:
1 | public class Student { |
我们希望在增强的方法中也能拿到这个参数,然后进行处理:
1 | public class StudentAOP { |
这个时候,我们可以为我们切入的方法添加一个JoinPoint参数,通过此参数就可以快速获取切点位置的一些信息:
1 | public void afterStudy(JoinPoint point) { //JoinPoint实例会被自动传入 |
接着我们修改一下刚刚的AOP配置(因为方法参数有变动)看看结果吧:
1 | <aop:pointcut id="test" expression="execution(* org.example.entity.Student.study(String))"/> |
现在我们来测试一下:
1 | public static void main(String[] args) { |
是不是感觉大部分功能都可以通过AOP来完成了?
我们接着来看自定义度更高的环绕方法,现在我们希望在方法执行前和执行后都加入各种各样的动作,如果还是一个一个切点写,有点太慢了,能不能直接写一起呢,此时我们就可以使用环绕方法。
环绕方法相当于完全代理了此方法,它完全将此方法包含在中间,需要我们手动调用才可以执行此方法,并且我们可以直接获取更多的参数:
1 | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { |
注意,如果代理方法存在返回值,那么环绕方法也需要有一个返回值,通过proceed
方法来执行代理的方法,也可以修改参数之后调用proceed(Object[])
,使用我们给定的参数再去执行:
1 | public Object around(ProceedingJoinPoint joinPoint) throws Throwable { |
这里我们还是以study
方法为例,现在我们希望在调用前修改这个方法传入的参数值,改成我们自己的,然后在调用之后对返回值结果也进行处理:
1 | public String study(String str){ |
现在我们编写一个环绕方法,对其进行全方面处理:
1 | Object around(ProceedingJoinPoint joinPoint) throws Throwable { |
同样的,因为方法变动了,现在我们去修改一下我们的AOP配置:
1 | <aop:pointcut id="test" expression="execution(* org.example.entity.Student.study(String))"/> |
细心的小伙伴可能会发现,环绕方法的图标是全包的,跟我们之前的图标不太一样。
现在我们来试试看吧:
1 | public static void main(String[] args) { |
这样,我们就实现了环绕方法,通过合理利用AOP带来的便捷,可以使得我们的代码更加清爽和优美。这里介绍一下 AOP 领域中的特性术语,防止自己下来看不懂文章:
- 通知(Advice): AOP 框架中的增强处理,通知描述了切面何时执行以及如何执行增强处理,也就是我们上面编写的方法实现。
- 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出,实际上就是我们在方法执行前或是执行后需要做的内容。
- 切点(PointCut): 可以插入增强处理的连接点,可以是方法执行之前也可以方法执行之后,还可以是抛出异常之类的。
- 切面(Aspect): 切面是通知和切点的结合,我们之前在xml中定义的就是切面,包括很多信息。
- 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
- 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,我们之前都是在将我们的增强处理添加到目标对象,也就是织入(这名字挺有文艺范的)
使用接口实现AOP
前面我们介绍了如何使用xml配置一个AOP操作,这节课我们来看看如何使用Advice实现AOP。
它与我们之前学习的动态代理更接近一些,比如在方法开始执行之前或是执行之后会去调用我们实现的接口,首先我们需要将一个类实现Advice接口,只有实现此接口,才可以被通知,比如我们这里使用MethodBeforeAdvice
表示是一个在方法执行之前的动作:
1 | public class StudentAOP implements MethodBeforeAdvice { |
我们发现,方法中包括了很多的参数,其中args代表的是方法执行前得到的实参列表,还有target表示执行此方法的实例对象。运行之后,效果和之前是一样的,但是在这里我们就可以快速获取到更多信息。还是以简单的study方法为例:
1 | public class Student { |
1 | <bean id="student" class="org.example.entity.Student"/> |
我们来测试一下吧:
除了此接口以外,还有其他的接口,比如AfterReturningAdvice
就需要实现一个方法执行之后的操作:
1 | public class StudentAOP implements MethodBeforeAdvice, AfterReturningAdvice { |
因为使用的是接口,就非常方便,直接写一起,配置文件都不需要改了:
我们也可以使用MethodInterceptor(同样也是Advice的子接口)进行更加环绕那样的自定义的增强,它用起来就真的像代理一样,例子如下:
1 | public class Student { |
1 | public class StudentAOP implements MethodInterceptor { //实现MethodInterceptor接口 |
我们来看看结果吧:
使用起来还是挺简单的。
使用注解实现AOP
接着我们来看看如何使用注解实现AOP操作,现在变回我们之前的注解开发,首先我们需要在主类添加@EnableAspectJAutoProxy
注解,开启AOP注解支持:
1 |
|
还是熟悉的玩法,类上直接添加@Component
快速注册Bean:
1 |
|
接着我们需要在定义AOP增强操作的类上添加@Aspect
注解和@Component
将其注册为Bean即可,就像我们之前在配置文件中也要将其注册为Bean那样:
1 |
|
接着,我们可以在里面编写增强方法,并将此方法添加到一个切点中,比如我们希望在Student的study方法执行之前执行我们的before
方法:
1 | public void before(){ |
那么只需要添加@Before注解即可:
1 | //execution写法跟之前一样 |
这样,这个方法就会在指定方法执行之前执行了,是不是感觉比XML配置方便多了。我们来测试一下:
1 | public static void main(String[] args) { |
同样的,我们可以为其添加JoinPoint
参数来获取切入点信息,使用方法跟之前一样:
1 |
|
为了更方便,我们还可以直接将参数放入,比如:
1 | public void study(String str){ |