Spring是Java SE/EE 应用的full-stack的(不是只能应用于某个层,而是在表现层、业务层、持久层都可以应用)轻量级开源框架。IoC(Inverse Of Control: 反转控制)和 AOP(Aspect Oriented Programming:面向切面编程)是Spring框架的核心。
IoC
1. IoC的作用:削减计算机程序模块之间的耦合(削减模块间的依赖)。
耦合:如,
这里在类A的程序中new了另一个类(B_impl),造成了这两个类之间的耦合(类A依赖于类B_impl)。那么当A需要使用不同的B实现类时,就必须要修改源代码去new另一个实现类,造成不便。
2. Spring控制反转机制的底层实现:工厂+配置文件+反射
这个工厂类在Spring中被抽象为BeanFactory接口,它常用的实现类有,
ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,它们分别用于从类路径下和磁盘路径下读取配置文件来创建一个工厂,例:
这里ApplicationContext也是工厂,它是BeanFactory的子接口(一般使用ApplicationContext)。然后调用工厂的getBean方法来获得实例:
--不使用Spring:类A程序内部去new自己所需要的依赖(类B_impl)(控制权在类A自己手中,类A主动去拿类B_impl)。
--使用Spring:类A所需要的依赖(类B_impl)是从类A程序的外部注入给类A的(控制权反转给外界,类A被动接受外界所注入的类,类A并不知道自己会拿到什么,由外界控制)。
通过控制反转机制,类A的源代码已经跟B_impl类完全没关系了(B_impl被写在了配置文件中交给Spring管理了)。那么当需要使用不同的B的实现类时就不需要更改A中源代码了,只需要对配置文件进行修改,这就消减了类A和类B_impl之间的耦合。
3. 控制反转与依赖注入(Dependency Injection,DI):不作区分。只是侧重强调不同的方面。控制反转强调的是对象创建的控制权由程序反转给了外界(Spring)。依赖注入强调的是程序中所需要的依赖不是由自己创建,而是由外界(Spring)注入。
4. 配置文件中bean标签的scope属性(作用域)的配置:
当bean的scope为单例singleton时(默认),Spring只会为我们创建一个B_impl对象,每次调用getBean方法返回的都是同一个对象。
当bean的scope为多例prototype时,每次调用getBean方法,Spring都会为我们创建一个新的B_impl对象返回。
5. 获取bean实例的三种方式:
方式一,通过调用B_impl类默认的构造函数来获得B的实例(当B_impl没有默认的构造函数时将会报错)(若想调用其有参的构造函数,则需要依赖注入将参数注入,后讲)
方式二,当我们希望通过某个类中的会返回B实例的静态方法来获得B实例时(如类C是一个工厂,它的一个静态方法static_getB会返回B实例)。
方式三,当我们希望通过某个类中会返回B实例的非静态方法来获得B实例时。
6. 注入依赖的两种方式:
(i). 构造函数注入:
调用类A的构造函数,构造函数的第一个参数传"hello",第二个参数传B_impl对象。
(ii). set方法注入:
Spring会调用类A中的setStr方法将"hello"注入,调用setB方法将B_impl注入。
对于集合的注入:
左:向setB_array方法传入一个包含三个b_impl对象引用的数组。
中:向setStr_array方法传入一个包含三个"hello"字符串的数组。
右:向setB_map方法传入一个包含一个键值对(key为"hello",value为b_impl)的map。
如果要传入的不是数组而是List、Set,或者不是Map而是Properties也是采用相同的配置。
7. 基于注解的IoC配置
等价于
@Component还有三个衍生注解@Controller@Service@Repository,其与@Component一样,只不过专门用来注解表现层,业务层,持久层。
等价于
等价于
注解方式的依赖注入不需要相应的构造函数和setter方法,直接在成员变量上加注解。
对于基本数据类型的注入,采用@Value注解(集合类的注入请采用配置文件的方式)。
对于非基本数据类型的注入有三种方式,
左:去所有的bean中找类型为B的bean,若有多个类型为B的bean,则找id为"b"的bean。
中:找id为"B"的bean
右:找id为"B"的bean(采用了javax.annotation包中的注解,而不是spring框架中的注解)
补:BeanFactory和ApplicationContext的主要区别在于,使用BeanFactory,只有在调用getBean的时候其对应的那个bean才会被实例化;而使用ApplicationContext,在创建工厂时,配置文件中所有的单例bean都会被实例化。
AOP
1. 问题来源:
好的代码应该避免重复性的代码编写。如果某段代码在程序中反复被使用到,则这段代码应该被抽象封装出来。
对于某一类代码,如方法计时代码(计算某方法运行的时间,需要在方法的第一行和最后一行添加代码来进行时间的记录)、权限检查代码(需要在方法的第一行添加代码来进行权限的检验)、日志记录代码(需要在方法的最后一行添加代码来进行日志的记录),这些代码具有一定的通用性和独立性。例如,无论对什么方法进行计时,其添加的计时代码都是相同的,与被计时的方法并没有任何关系。
在所有需要计时的方法中都去重复书写相同的计时代码,会造成代码的冗余。为了解决这一问题,我们需要将这一部分计时的代码抽象出来封装起来。然而,无论是权限检查代码、日志记录代码、方法计时代码,它们都是一些代码片段,被放置于方法的前面、后面、前面和后面。因此,我们需要面向的是代码片段(切面)进行编程(Aspect Oriented Programming, AOP),将这些代码片段抽离出来,削减其与原本程序的耦合。
2. 面向切面编程思想是面向对象编程思想的延伸而不是替代。可以理解为aop是进一步的将代码片段抽象为了对象。
3. Java中对AOP思想的实现:代理模式
代理模式包含三个角色:抽象角色(一系列接口),真实角色(为抽象角色的一系列接口提供了具体实现,这些实现就是主要的业务逻辑),代理角色(也实现了抽象角色的那一系列接口,但是在实现时,对于主要的业务逻辑是调用了真实角色中的实现,自己只是在真实角色的实现的基础上进行一些代码片段的添加)。
例:
这里定义了接口A,B(抽象角色),类RealSubject(真实对象,实现了接口A,B)。
静态代理:
类ProxySubject(代理角色)也对接口A,B进行了实现。Object o用于存真实角色对象(因为真实角色可能会实现多个接口,所以这里用Object类来接收)。在实现a/b_method方法时,它直接调用了真实角色对象中的实现方法,只是在其前后添加了代码片段。
当想调用抽象角色中的某个方法时(如A中的a_method),我们不直接调用真实角色对象(a)自己的a_method方法,而是先用该真实角色对象(a)去生成它的代理(a_proxy),然后调用代理的a_method方法。因此,实际执行的是经过了代码片段添加后的代码。
静态代理还是存在一定冗余。对于不同接口的不同方法,其都要给出重复的实现。如本例中的代理类是专门用于添加"before"和"after"输出的,其中的a/b_method方法添加的都是相同的代码。对于该问题,动态代理给出了解决。
动态代理:
在动态代理中,前面的代理角色ProxySubject不用再由我们自己去编写了,Proxy类的newProxyInstance方法会先自动帮我们生成这个类,然后再new出这个类的实例。该方法的第一个参数接收一个ClassLoader,用来装载生成的代理类;第二个参数接收一组接口作为抽象角色,生成的代理类会去自动实现这组接口;第三个参数是一个实现了InvocationHandler接口的实例,那么自动生成的代理类里,对抽象角色的那一系列接口的实现,实际上就是调用的InvocationHandler接口的Invoke方法。Handler类的编写和前面的ProxySubject类的编写类似,只不过在invoke方法中采用了反射的方式去调用真实角色o的方法(方法中的proxy参数表示的是调用该方法的代理类的实例,即a_proxy)。
4. AOP的强大之处:在不对方法中的源码进行修改的前提下,就对该方法进行了功能的增加。
5. 在Spring中使用AOP:
自定义一个通知类,里面的方法被也称为通知(Advice),方法中编写的就是需要增加的代码。这里定义的around方法中编写的是需要在原方法周围增加的代码(环绕通知);before中编写的是需要在原方法之前增加的代码(前置通知);after_returning中编写的是需要在原方法之后增加的代码(后置通知);after_throwing中编写的是需要在原方法出现异常时执行的代码(就是catch中的代码)(异常通知);after中编写的是需要在原方法出现异常后执行的代码(就是finally中的代码)(最终通知)。
织入后的伪代码:
6. 使用注解配置
7. Spring中的事务控制
现在有一个业务层中的方法需要被作为一个事务(事务是业务层中的):
相关配置(并不需要对源码进行任何的代码添加,仅靠这些配置,就可以完成事务的添加):
补:事务的传播行为
作用:spring中的事务定义在业务层的方法上,当业务层的方法间互相调用时,就会导致不同事务的嵌套。事务的传播行为则定义了当这种情况发生时的不同应对方法。
常用的事务传播行为有:REQUIRED, REQUIRES_NEW, NESTED(实际上共有七种传播行为)
REQUIRED:当method_A的传播行为为REQUIRED时,若method_B没配置事务,则spring会开启一个新的事务,将method_A/B都放进这个事务中;若method_B配置了事务,则method_A被会加入到method_B的事务中。(method_A/B无论如何都在同一个事务中)
REQUIRES_NEW:当method_A的传播行为为REQUIRES_NEW时,无论method_B有没有配置事务,spring都会开启一个新的事务,并仅将method_A放入到这个新建事务中。若method_B没配置事务,则就只有method_A有事务;即使method_B配置了事务,method_A/B也是分别存在于两个不同的事务中。method_A中若发生异常,则只回滚method_A中执行的操作,不回滚method_B中的操作;method_B中若发生异常,则只回滚method_B中执行的操作,不回滚method_A中的操作。(method_A/B无论如何都不在同一个事务中)
NESTED:当method_A的传播行为为NESTED时,若method_B没配置事务,则spring会开启一个新的事务,将method_A/B都放进这个事务中(同REQUIRED)。若method_B配置了事务,则method_A的事务会成为method_B的事务的子事务。主事务中若发生异常,则子事务中执行的操作也会一起回滚;子事务中若发生异常,则仅会回滚自己事务内执行的操作,而主事务处可以捕获子事务抛出的异常,从而选择是否回滚主事务中执行的操作。
8. 使用注解配置:
(仍旧要在配置文件中配置事务管理器的bean)
该注解也可以加在类或者接口上表示事务将添加给该类的所有方法上或者该接口的所有实现类上。