《Spring实战》阅读笔记

简化 Java 开发

为了降低 Java 开发的复杂性, Spring 采取了以下 4 种关键策略:

  • 基于POJO (Plain Old Java object) 的轻量级和最小侵入性编程
  • 通过依赖注入和面向接口实现松耦合
  • 基于切面和惯例进行声明式编程
  • 通过切面和模板减少样板式代码

依赖注入 (DI)

传统做法下,每个对象负责管理与自己所依赖的对象的引用,这样会导致高耦合。

通过依赖注入,对象的依赖关系将由一个第三方组件在创建对象时进行自动注入,对象无需管理它们的依赖关系。
依赖注入

依赖注入的实现方式:

  • 构造器注入:在构造方法中将依赖对象作为参数传入
  • XML
  • 注解

Spring 通过 Application Context 装载 bean 的定义并负责对象的创建与组装。

面向切面编程 (AOP)

AOP 允许将遍布应用各处的重复功能,分离出来形成可重用的组件。

Spring 提供了 4 种类型的 AOP 支持:

  • 基于代理的经典 Spring AOP
  • 纯 POJO切 面
  • @AspectJ 注解驱动的切面
  • 注入式 AspectJ 切面(适用于 Spring 各版本)

Weaving (织入)

织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多个点
可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time
    weaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的

Aspect (切面)

切面是 Advice 和 Pointcut 的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

Advice (通知)

即切面的具体工作,Advice定义了切面是什么以及何时使用。除了描述切面要完成的工作,
通知还解决了何时执行这个工作的问题。

Spring 中的 AOP 定义了以下5种 Advice:

  • 前置通知(Before):在目标方法被调用之前调用通知功能
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
  • 返回通知(After-returning):在目标方法成功执行之后调用通知
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为

Joint point (连接点)

连接点是在应用执行过程中能够插入切面的一个点。

这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

Spring 只支持方法级别的 Joint point。

Pointcut (切点)

如果说 Advice 定义了切面的“什么”和“何时”的话,那么 Pointcut 就定义了“何处”。

切点的定义会匹配通知所要织入的一个或多个 Joint point。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

有些 AOP 框架还允许创建动态的切点。

Spring 容器

Spring 容器负责创建、装配与配置对象,并管理对象的整个生命周期(从 new 到 finalize( ))。

Spring 自带了不同类型的容器实现;

  • Bean工厂,是最简单的容器,提供基本DI支持。

  • 应用上下文 (Application Context),基于BeanFactory构建,提供面向应用的服务,例如,解析属性文本信息内容。

Application Context

Spring 自带了多种 Application Context 的实现,区别仅在于如何加载配置。

  • AnnotationConfigApplicationContext:从一个或多个基于 Java 的配置类中加载 Spring 应用上下文。
  • ClassPathXmlApplicationContext:从一个或多个基于 Java 的配置类中加载 Spring Web 应用上下文。
  • FileSystemXmlApplicationContext:从类路径下的一个或多个 XML 配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
  • XmlWebApplicationContext:从 Web 应用下的一个或多个 XML 配置文件中加载上下文定义。

Bean 的生命周期

bean

  1. Spring对bean进行实例化;
  2. Spring将值和bean的引用注入到bean对应的属性中;
  3. 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法;
  4. 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
  5. 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;
  6. 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;
  7. 如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用;
  8. 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法;
  9. 此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
    10.如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。

Spring 中的 Bean

Spring 中提供了三种主要的装配机制:

  • 在XML中进行显式配置。
  • 在Java中进行显式配置。
  • 隐式的 bean 发现机制和自动装配

其中自动装配是通过以下两种方式来实现的:

  • 组件扫描(component scanning):Spring会自动发现应用上下文
    中所创建的bean。
  • 自动装配(autowiring):Spring自动满足bean之间的依赖。

声明 Bean

使用@Component注解,解表明该类会作为组件类,并告知Spring要为这个类创建 bean。

然后使用@ComponentScan注解开启组件扫描,默认情况下它将以配置类所在的package作为base package来扫描组件。

消除 Autowired 的歧义性

通过 @Primary 配置首选自动装配的 bean 或通过 @Qualifier 来限定自动装配的 bean。

Bean 的作用域

默认情况下,Spring ApplicationContext 中所有 bean 都是作为单例的形式创建的。

通过 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 注解可以设置其他的作用域,或者是@Scope('prototype'),但是用 SCOPE_PROTOTYPE 常量更加安全不易出错。

Spring 定义了多种作用域,可以基于这些作用域创建 bean,包括:

  • 单例(Singleton):在整个应用中,只创建 bean 的一个实例。
  • 原型(Prototype):每次注入或者通过 Spring ApplicationContext 获取的时候,都会创建一个新的 bean 实例。
  • 会话(Session):在 Web 应用中,为每个会话创建一个 bean 实例。
  • 请求(Request):在 Web 应用中,为每个请求创建一个 bean 实例。

会话作用域与请求作用域

设想一个电商场景中的一个 bean 代表用户的购物车,如果这个 bean 是单例的,那么所有用户都会向同个购物车中添加商品,如果是原型的,在应用中某个地方添加了商品,在另一个地方可能就拿不到了。在这个场景下,会话作用域是最为合适的。

使用会话作用域:

1
2
3
4
5
6
7
// SCOPE_SESSION常量告诉Spring为每个回话创建一个ShoppingCart bean的实例
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
public ShoppingCart shoppingCart() {
...
}

注意一点,@Scope注解同时还有一个proxyMode属性,这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。

如果将一个会话作用域的bean注入到一个单例bean中,只有某个用户发起会员,这个单例bean才会被注入依赖的会话作用域bean。当注入时,Spring 并不会将实际的会话作用域bean注入到单例bean中,而是注入到单例bean的代理,如下图所示。

这个代理会暴露与会话作用域bean相同的方法,当单例bean调用会话作用域bean的方法时,代理会对其进行懒解析并将调用委托给对应的会话作用域bean。

上面的proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现接口,并将调用委托给实现bean。如果对应的bean不是接口而是类的话,则需要设置为ScopedProxyMode.TARGET_CLASS

  • 本文作者: Marticles
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!