Spring学习之旅
前言
用于记录日常学习,资料多数来源于掘金小册从 0 开始深入学习 Spring。
一、如何理解IOC和AOP?为什么使用IOC?
1.1 控制反转IOC
IOC全称为Inversion of Control,即为控制反转,目的是为了借助于“第三方”实现具有依赖关系的对象之间的解耦:
针对新建一个对象,有以下两种方式:
- 方式1:直接创建对象
private DemoDao dao = new DemoDaoImpl();
利用方式1创建N个对象后,各对象之间相互合作实现系统整体逻辑,可用下图形象表述:
即各对象间紧密耦合,如果有一个对象出现问题,都会影响整个系统的正常运转。
- 方式2:以IOC方式创建对象
private DemoDao dao = (DemoDao) BeanFactory.getBean("demoDao");
利用方式2,如果对象A依赖对象B,则需要通过借助“第三方”(IOC容器)来保持对象之间的关系,有效降低了对象间的耦合度,可用下图形象表述
通过这两种方式的对比,不难看出:利用IOC方式创建的对象,对象A在获取依赖对象B的过程由主动变成了被动,控制权颠倒过来了,这就是“控制反转”。
1.2 面向切面编程 AOP
AOP是一种编程思想,与OOP (面向对象编程)不同, OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。
1.2.1 业务场景
针对某一积分变换的业务场景,其业务组件DemoService
包含以下几个方法:
1. public interface DemoService {
2. List<String> findAll();
3.
4. int add(String userId, int points);
5. int subtract(String userId, int points);
6. int multiply(String userId, int points);
7. int divide(String userId, int points);
8. }
该接口的实现类DemoServiceImpl
为
1. @Override
2. public int add(String userId, int points) {
3. return points;
4. }
5.
6. @Override
7. public int subtract(String userId, int points) {
8. return points;
9. }
10.
11. @Override
12. public int multiply(String userId, int points) {
13. return points;
14. }
15.
16. @Override
17. public int divide(String userId, int points) {
18. return points;
19. }
如果我们想对所有的方法添加打印日志的功能,那么实现类DemoServiceImpl
变为
1. @Override
2. public int add(String userId, int points) {
3. LogUtils.printLog("DemoServiceImpl", "add", userId, points);
4. return points;
5. }
6.
7. @Override
8. public int subtract(String userId, int points) {
9. LogUtils.printLog("DemoServiceImpl", "subtract", userId, points);
10. return points;
11. }
12.
13. // 省略multiply与divide......
该实现类的代码结构为:
不难发现,对于安全检查、日志记录、事务等代码会重复出现在每个业务方法中:
这些方法的开始 / 结束都有相同的逻辑,那我们就可以把这些逻辑都拿出来视为一体,这个思想就叫横切,提取出来的逻辑组成的虚拟的结构,我们可以称之为横切面(上图的红框就可以理解为一个横切面)
对于传统的OOP方式,很难将这些相同的逻辑代码进行模块化处理。因此,我们利用切面的思想进行处理,动态代理就是切面编程很好地体现。
1.2.2 动态代理
动态代理是在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。
让 Servlet 在初始化的时候,从 BeanFactory 中获取 DemoService ,然后借助 jdk 动态代理生成 DemoService 的代理对象,并给其中的方法增强:
1. public class DemoServlet10 extends HttpServlet {
2.
3. DemoService demoService;
4.
5. @Override
6. public void init() throws ServletException {
7. DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
8. Class<? extends DemoService> clazz = demoService.getClass();
9. // 使用jdk动态代理,生成代理对象
10. this.demoService = (DemoService) Proxy
11. .newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), (proxy, method, args) -> {
12. LogUtils.printLog("DemoService", method.getName(), args);
13. return method.invoke(demoService, args);
14. });
15. }
16. }
二、如何使用SpEL表达式?
SpEL 全称 Spring Expression Language,它从 SpringFramework 3.0 开始被支持,它本身可以算 SpringFramework 的组成部分,但又可以被独立使用。它可以支持调用属性值、属性参数以及方法调用、数组存储、逻辑计算等功能。
- SpEL 的语法统一用
#{ }
表示,花括号内部编写表达式语言。
2.1 SpEL属性注入
创建一个 Blue
,声明 name
和 order
,并提供 getter
、setter
方法(为了方便后续操作)和 toString()
方法,最后用 @Component
标注,使用 @Value
配合 SpEL 完成字面量的属性注入,需要额外在花括号内部加单引号:
1. @Component
2. public class Blue {
3.
4. @Value("#{'blue-value-byspel'}")
5. private String name;
6.
7. @Value("#{2}")
8. private Integer order;
9. }
通过Bean的依赖注入,可以获取到:
1. Blue{name='blue-value-byspel', order=2}
2.2 SpEL属性引用
创建一个 Green
,以同样的方式对字段和方法进行声明,同时标注 @Component
注解:
1. @Component
2. public class Green {
3.
4. @Value("#{'copy of ' + blue.name}")
5. private String name;
6.
7. @Value("#{blue.order + 1}")
8. private Integer order;
9. }
通过对Green注入,可得
1. use spel bean property : Green{name='copy of blue-value-byspel', order=3}
2.3 方法调用
创建White
类,@Value
标记的属性,并调用方法处理
1. @Component
2. public class White {
3.
4. @Value("#{blue.name.substring(0, 3)}")
5. private String name;
6.
7. @Value("#{T(java.lang.Integer).MAX_VALUE}")
8. private Integer order;
9. }
直接引用类的属性,需要在类的全限定名外面使用 T( )
包围。
- use spel methods : White{name='blu', order=2147483647}
三、Spring AOP和 AspectJ AOP的区别
AOP:面向切面编程,全称 Aspect Oriented Programming ,它是 OOP 的补充。OOP 关注的核心是对象,AOP 的核心是切面(Aspect)。AOP 可以在不修改功能代码本身的前提下,使用运行时动态代理的技术对已有代码逻辑增强。AOP 可以实现组件化、可插拔式的功能扩展,通过简单配置即可将功能增强到指定的切入点。
3.1 AOP基础术语
针对某一业务场景,该场景包含两种业务,分别为
1、账号充值
2、账号解封
在这个场景中,左边的主管视为 “原始对象”,主管可提供账户充值、账号解封等业务,意为一个 Class 中定义的几个方法;中间的业务经理视为 “中间的代理层”,他平时招揽客人,并且将客人的需求传达给里面的主管;右边开门办业务的视为 “客户端”,办业务的时候都是由它发起。
利用代理层去代理原始对象的过程可用以下代码表现:
1. public static Partner getPartner(int money) {
2. // partner即为目标对象
3. Partner partner = partners.remove(0);
4. return (Partner) Proxy.newProxyInstance(......);
5. }
该过程有以下几个术语需要掌握:
- Target 目标对象
目标对象就是被代理的对象,代码中partner
即为被代理的对象,对应上图左边的主管
- Proxy 代理对象
代理对象就是上面代码中 Proxy.newProxyInstance
返回的结果。上图中,中间的业务经理 + 左边的主管,组合形成一个代理对象(代理对象中还包含原始对象本身)。
- JoinPoint 连接点
目标对象的所属类中,定义的所有方法,对应图中主管提供的几项业务(账号充值、账户解封)就属于连接点。
- Pointcut 切入点
是指那些被拦截 / 被增强的连接点。中间的业务经理在给主管传话的时候,并不是每次都实话实说,但也不都是瞎说,很明显他是看到有充值这样的涉及钱的业务,就开始胡说八道了,而没有涉及到钱的业务,他就如实转述。那我们是不是可以这样去理解:代理层会选择目标对象的一部分连接点作为切入点,在目标对象的方法执行前 / 后作出额外的动作。
因此,切入点可以是 0 个或多个(甚至全部)连接点的组合。
- Advice 通知
增强的逻辑,也就是增强的代码。业务经理发现有人要充值的时候,它并没有直接传话给主管,而是先执行了他自己的逻辑:胡说八道,而在传话之前的这个胡说八道,就是业务主管针对账户充值这个连接点的增强逻辑。因此,Proxy 代理对象 = Target 目标对象 + Advice 通知
切入点和通知是要配合在一起使用的,有了切入点之后,需要搭配上增强的逻辑,才能算是给目标对象进行了代理、增强。
- Aspect 切面
Aspect 切面 = PointCut 切入点 + Advice 通知
- Weaving 织入
织入就是将 Advice 通知应用到 Target 目标对象,进而生成 Proxy 代理对象的过程。相当于Proxy 代理对象 = Target 目标对象 + Advice 通知中的+号
3.2 Spring AOP
Spring AOP实现是利用JDK动态代理以及Cglib动态代理实现的
3.2.1 JDK动态代理
jdk 的动态代理,要求被代理的对象所属类必须实现一个以上的接口,代理对象的创建使用 Proxy.newProxyInstance
方法,该方法中有三个参数:
ClassLoader loader
:被代理的对象所属类的类加载器Class<?>[] interfaces
:被代理的对象所属类实现的接口InvocationHandler h
:代理的具体代码实现
在这三个参数中,前面两个都容易理解,最后一个 InvocationHandler
是一个接口,它的核心方法 invoke
中也有三个参数:
Object proxy
:代理对象的引用(代理后的)Method method
:代理对象执行的方法Object[] args
:代理对象执行方法的参数列表
具体的代理逻辑就在 `InvocationHandler` 的 `invoke` 方法中编写:
- public static Partner getPartner(int money) {
- Partner partner = partners.remove(0);
- return (Partner) Proxy.newProxyInstance(partner.getClass().getClassLoader(), partner.getClass().getInterfaces(),
- new InvocationHandler() {
- private int budget = money;
- private boolean status = false;
-
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- if (method.getName().equals("receiveMoney")) {
- int money = (int) args[0];
- // 平台需要运营,抽成一半
- args[0] = money / 2;
- // 如果在付钱时没给够,则标记budget为异常值
- this.status = money >= budget;
- }
- if (status) {
- return method.invoke(partner, args);
- }
- return null;
- }
- });
- }
3.2.2 Cglib动态代理
Cglib 动态代理的内容相对较少,它只需要传入两个东西:
Class type
:被代理的对象所属类的类型Callback callback
:增强的代码实现
由于一般情况下我们都是对类中的方法增强,所以在传入 Callback
时通常选择这个接口的子接口 MethodInterceptor
(所以也就有了代码中 new
的 MethodInterceptor
的匿名内部类)。
1. public static Partner getPartner(int money) {
2. Partner partner = partners.remove(0);
3. // 使用Cglib的Enhancer创建代理对象
4. return (Partner) Enhancer.create(partner.getClass(), new MethodInterceptor() {
5. private int budget = money;
6. private boolean status = false;
7.
8. @Override
9. public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
10. throws Throwable {
11. // 如果在付钱时没给够,则标记budget为异常值
12. if (method.getName().equals("receiveMoney")) {
13. int money = (int) args[0];
14. this.status = money >= budget;
15. }
16. if (status) {
17. return method.invoke(partner, args);
18. }
19. return null;
20. }
21. });
22. }
3.3 AspectJ AOP
Spring Framework通过整合AspectJ来实现基于注解配置的AOP,Spring Framework中的通知类型是基于AspectJ定制的:
Before
前置通知:目标对象的方法调用之前触发After
后置通知:目标对象的方法调用之后触发AfterReturning
返回通知:目标对象的方法调用完成,在返回结果值之后触发AfterThrowing
异常通知:目标对象的方法运行中抛出 /触发异常后触发Around
环绕通知:编程式控制目标对象的方法调用
3.3.1 以类全名作为切点
在 Logger 上标注 @Component
注解,将其注册到 IOC 容器中。然后还得标注一个 @Aspect
注解,代表该类是一个切面类:
1. @Aspect
2. @Component
3. public class Logger { ... }
然后在切面方法中标记通知类型,及需要通知的对象方法"execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"
1. @Aspect
2. @Component
3. public class Logger {
4.
5. @Before("execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))")
6. public void beforePrint() {
7. System.out.println("Logger beforePrint run ......");
8. }
9. }
编写配置类,在配置中标记@EnableAspectJAutoProxy
开启AOP注解
1. @Configuration
2. @ComponentScan("com.linkedbear.spring.aop.b_aspectj")
3. @EnableAspectJAutoProxy
4. public class AspectJAOPConfiguration {
5.
6. }
Around包含了Before
、After
、AfterReturning
和AfterThrowing
四个过程,在JDK动态代理以及Cglib动态代理中, InvocationHandler
和 MethodInterceptor
的编写本身就是环绕通知的体现。
在切面方法中添加@Around
注解来表示环绕通知:
1. @Around("execution(public * com.linkedbear.spring.aop.b_aspectj.service.FinanceService.addMoney(..))")
2. public Object aroundPrint(ProceedingJoinPoint joinPoint) throws Throwable {
3. System.out.println("Logger aroundPrint before run ......");
4. try {
5. Object retVal = joinPoint.proceed();
6. System.out.println("Logger aroundPrint afterReturning run ......");
7. return retVal;
8. } catch (Throwable e) {
9. System.out.println("Logger aroundPrint afterThrowing run ......");
10. throw e;
11. } finally {
12. System.out.println("Logger aroundPrint after run ......");
13. }
14. }
ProceedingJoinPoint
有一个 proceed
方法,执行了它,就相当于之前咱在动态代理中写的 method.invoke(target, args);
方法了,即调用代理方法。
3.3.2 ASpectJ注解抽取通用切入点
在注解 AOP 切面中,定义通用的切入点表达式只需要声明一个空方法,并标注 @Pointcut
注解即可:
1. @Pointcut("execution(* com.linkedbear.spring.aop.b_aspectj.service.*.*(String)))")
2. public void defaultPointcut() {
3.
4. }
其它的通知要引用这个切入点表达式,只需要标注方法名即可,效果是一样的:
10. @Aspect
11. @Component
12. public class Logger {
13.
14. @Before("defaultPointcut()")
15. public void beforePrint() {
16. System.out.println("Logger beforePrint run ......");
17. }
3.3.3 以注解形式获取切点
继续使用Logger作为切面类,定义@Log
注解,用于标注要打印日志的方法:
1. @Documented
2. @Retention(RetentionPolicy.RUNTIME)
3. @Target(ElementType.METHOD)
4. public @interface Log {
5.
6. }
修改切入点的表达式:
1. @Pointcut("@annotation(com.XXX.xxx.xxxx.Log)")
2. public void defaultPointcut() {
3.
4. }
以此法声明的切入点表达式会搜索整个 IOC 容器中标注了 @Log
注解的所有 bean 全部增强。
利用@Log
标记需要打印日志的方法:
1. @Log
2. public double subtractMoney(double money) {
3. System.out.println("FinanceService 付钱 === " + money);
4. return money;
5. }
3.4 Spring AOP和ASpectJ AOP对比
四、IOC容器对比(BeanFactory
和Application
)
IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装。在Spring中BeanFactory
是IOC容器的实际代表者。
在Spring IOC容器的代表就是org.springframework.beans
包中的BeanFactory
接口,BeanFactory
接口提供了IOC容器最基本功能;而org.springframework.context
包下的ApplicationContex
t接口扩展了BeanFactory
,还提供了与Spring AOP集成、国际化处理、事件传播及提供不同层次的context实现 (如针对web应用的WebApplicationContext
)。简单说,BeanFactory
提供了IOC容器最基本功能,而 ApplicationContext
则增加了更多支持企业级功能支持。ApplicationContext
完全继承BeanFactory
,因而BeanFactory
所具有的语义也适用于ApplicationContext
。
五、不同方式依赖注入的对比
依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念。具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。
5.1 构造器注入
构建Person类,并创建全参的构造函数
1. public Person(String name, Integer age) {
2. this.name = name;
3. this.age = age;
4. }
在配置类中,注册Bean
1. @Bean
2. public Person person() {
3. return new Person("test-person-anno-byconstructor", 18);
4. }
5.2 setter注入
1. @Bean
2. public Person person() {
3. Person person = new Person();
4. person.setName("test-person-anno-byset");
5. person.setAge(18);
6. return person;
7. }
5.3 属性注入
1. <bean id="person" class="com.linkedbear.spring.basic_di.a_quickstart_set.bean.Person">
2. <property name="name" value="test-person-byset"/>
3. <property name="age" value="18"/>
4. </bean>
六、@Autowired
、@Resource
和@Inject
6.1注入方式对比
6.2 @Autowired
注解的使用
在 Bean 中直接在属性 / setter 方法上标注 @Autowired
注解,IOC 容器会按照属性对应的类型,从容器中找对应类型的 Bean 赋值到对应的属性上,实现自动注入。
(1)定义Person类及Dog类
1. @Component
2. public class Person {
3. private String name = "administrator";
4. // setter
1. @Component
2. public class Dog {
3.
4. @Value("dogdog")
5. private String name;
6.
7. private Person person;
8. // toString() ......
(2)给Dog注入Person的三种方式
6.2.1 在属性上标注
1.@Component
2. public class Dog {
3. // ......
4. @Autowired
5. private Person person;
6.2.2 构造器注入方式
1.@Component
2. public class Dog {
3. // ......
4. private Person person;
5.
6. @Autowired
7. public Dog(Person person) {
8. this.person = person;
9. }
6.2.3 setter 方法注入
1. @Component
2. public class Dog {
3. // ......
4. private Person person;
5.
6. @Autowired
7. public void setPerson(Person person) {
8. this.person = person;
9. }
七 多个切面的执行顺序
7.1 不同切面不同通知
一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的。
7.1.1 默认顺序
默认的切面执行顺序,是按照字母表的顺序来的。严谨来讲,是根据切面类的 unicode 编码,按照十六进制排序得来的
7.1.2 显式声明执行顺序
实现Ordered接口
1. @Component
2. @Aspect
3. public class TransactionAspect implements Ordered {
4.
5. @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
6. public void beginTransaction() {
7. System.out.println("TransactionAspect 开启事务 ......");
8. }
9.
10. @Override
11. public int getOrder() {
12. return 0;
13. }
14. }
@Order标记切面类
1. @Component
2. @Aspect
3. @Order(0)
4. public class LogAspect {
5.
6. @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
7. public void printLog() {
8. System.out.println("LogAspect 打印日志 ......");
9. }
10. }
在不显式声明 order 排序值时,默认的排序值是 Integer.MAX_VALUE,0优先级最高。
7.2 同一切面的不同通知
1. @Component
2. @Aspect
3. public class AbcAspect {
4.
5. @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
6. public void abc() {
7. System.out.println("abc abc abc");
8. }
9.
10. @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
11. public void def() {
12. System.out.println("def def def");
13. }
14. }
默认顺序
根据切面类的 unicode 编码,按照十六进制排序