简化 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 的生命周期
- Spring对bean进行实例化;
- Spring将值和bean的引用注入到bean对应的属性中;
- 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法;
- 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
- 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;
- 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;
- 如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用;
- 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法;
- 此时,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的实例
(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
。