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,声明 nameorder ,并提供 gettersetter 方法(为了方便后续操作)和 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 (所以也就有了代码中 newMethodInterceptor 的匿名内部类)。

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包含了BeforeAfterAfterReturningAfterThrowing四个过程,在JDK动态代理以及Cglib动态代理中, InvocationHandlerMethodInterceptor 的编写本身就是环绕通知的体现。

在切面方法中添加@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容器对比(BeanFactoryApplication

IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装。在Spring中BeanFactory是IOC容器的实际代表者。
在Spring IOC容器的代表就是org.springframework.beans包中的BeanFactory接口,BeanFactory接口提供了IOC容器最基本功能;而org.springframework.context包下的ApplicationContext接口扩展了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 编码,按照十六进制排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值