Spring

2017/04/24 JavaEE

Spring是一个开源的轻量级一站式(对三层都提供了解决技术)框架,核心IOC和AOP

功能:简化开发。创建Spring的主要目的是用来替代更加重量级的企业级Java技术,尤其是EJB。相对于EJB来说,Spring提供了更加轻量级和简单的编程模型。它增强了简单老式Java对象(Plain Old Java object,POJO)的功能,使其具备了之前只有EJB和其他企业级Java规范才具有的功能。桥梁作用,整合其他框架

Spring的价值:

  • Spring是一个非侵入性(non-invasive)框架,其目标是使应用程序代码对框架的依赖最小化,应用代码可以在没有Spring或者其他容器的情况下运行。
  • Spring提供了一个一致的编程模型,使应用直接使用POJO开发,从而可以与运行环境(如应用服务器)隔离开来。
  • Spring推动应用的设计风格向面向对象及面向接口编程转变,提高了代码的重用性和可测试性。
  • Spring改进了体系结构的选择,虽然作为应用平台,Spring可以帮助我们选择不同的技术实现,比如从Hiberante切换到其他ORM工具,从Struts切换到Spring MVC,尽管我们通常不会这样做,但是我们在技术方案上选择使用Spring作为应用平台,Spring至少为我们提供了这种可能性和选择,从而降低了平台锁定的风险。

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

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

Spring模块

spring-overview.png

核心依赖四个jar包:

  • core
  • beans
  • expression
  • context

  • Spring核心容器
    • 容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供了DI的功能。基于bean工厂,多种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。
    • 除了bean工厂和应用上下文,该模块也提供了许多企业服务,例如E-mail、JNDI访问、EJB集成和调度。
    • 所有的Spring模块都构建于核心容器之上。
  • Spring的AOP模块
    • 在AOP模块中,Spring对面向切面编程提供了丰富的支持。这个模块是Spring应用系统中开发切面的基础。与DI一样,AOP可以帮助应用对象解耦。借助于AOP,可以将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。
  • 数据访问与集成
    • Spring的JDBC和DAO(Data Access Object)模块抽象了样板式代码,使数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引发的问题。该模块在多种数据库服务的错误信息之上构建了一个语义丰富的异常层,再也不需要解释那些隐晦专有的SQL错误信息了!
    • Spring没有尝试去创建自己的ORM解决方案,而是对许多流行的ORM框架进行了集成,包括Hibernate、Java Persisternce API、Java Data Object和iBATIS SQL Maps。Spring的事务管理支持所有的ORM框架以及JDBC。
    • 本模块同样包含了在JMS(Java Message Service)之上构建的Spring抽象层,它会使用消息以异步的方式与其他应用集成。从Spring 3.0开始,本模块还包含对象到XML映射的特性,它最初是Spring Web Service项目的一部分。
    • 本模块会使用Spring AOP模块为Spring应用中的对象提供事务管理服务。
  • Web与远程调用
    • 虽然Spring能够与多种流行的MVC框架进行集成,但它的Web和远程调用模块自带了一个强大的MVC框架,有助于在Web层提升应用的松耦合水平。
    • 该模块还提供了多种构建与其他应用交互的远程调用方案。Spring远程调用功能集成了RMI(Remote Method Invocation)、Hessian、Burlap、JAX-WS,同时Spring还自带了一个远程调用框架:HTTP invoker。Spring还提供了暴露和使用REST API的良好支持。
  • Instrumentation
    • Spring的Instrumentation模块提供了为JVM添加代理(agent)的功能。
  • 测试
    • Spring提供了测试模块以致力于Spring应用的测试。通过该模块,你会发现Spring为使用JNDI、Servlet和Portlet编写单元测试提供了一系列的mock对象实现。对于集成测试,该模块为加载Spring应用上下文中的bean集合以及与Spring上下文中的bean进行交互提供了支持。

Spring Portfolio

整个Spring Portfolio包括多个构建于核心Spring框架之上的框架和类库。概括地讲,整个Spring Portfolio几乎为每一个领域的Java开发都提供了Spring编程模型。

  • Spring Web Flow
    • Spring Web Flow建立于Spring MVC框架之上,它为基于流程的会话式Web应用(可以想一下购物车或者向导功能)提供了支持。
  • Spring Web Service
    • 虽然核心的Spring框架提供了将Spring bean以声明的方式发布为Web Service的功能,但是这些服务是基于一个具有争议性的架构(拙劣的契约后置模型)之上而构建的。这些服务的契约由bean的接口来决定。 Spring Web Service提供了契约优先的Web Service模型,服务的实现都是为了满足服务的契约而编写的。
  • Spring Security
    • 安全对于许多应用都是一个非常关键的切面。利用Spring AOP,Spring Security为Spring应用提供了声明式的安全机制。
  • Spring Integration
    • 许多企业级应用都需要与其他应用进行交互。Spring Integration提供了多种通用应用集成模式的Spring声明式风格实现。
  • Spring Batch
    • 当需要对数据进行大量操作时,没有任何技术可以比批处理更胜任这种场景。如果需要开发一个批处理应用,你可以通过Spring Batch,使用Spring强大的面向POJO的编程模型。
  • Spring Data
    • Spring Data使得在Spring中使用任何数据库都变得非常容易。为NoSQL数据库提供了使用数据的新方法,这些方法会比传统的关系型数据库更为合适。
    • 不管你使用文档数据库,如MongoDB,图数据库,如Neo4j,还是传统的关系型数据库,Spring Data都为持久化提供了一种简单的编程模型。这包括为多种数据库类型提供了一种自动化的Repository机制,它负责为你创建Repository的实现。
  • Spring Social
    • 这是Spring的一个社交网络扩展模块。它能够帮助你通过REST API连接Spring应用
  • Spring Mobile
    • Spring Mobile是Spring MVC新的扩展模块,用于支持移动Web应用开发。
  • Spring for Android
    • 与Spring Mobile相关的是Spring Android项目。这个新项目,旨在通过Spring框架为开发基于Android设备的本地应用提供某些简单的支持。最初,这个项目提供了Spring RestTemplate的一个可以用于Android应用之中的版本。它还能与Spring Social协作,使得原生应用可以通过REST API进行社交网络的连接。
  • Spring Boot
    • Spring Boot是一个崭新的令人兴奋的项目,它以Spring的视角,致力于简化Spring本身。
    • Spring Boot大量依赖于自动配置技术,它能够消除大部分(在很多场景中,甚至是全部)Spring配置。它还提供了多个Starter项目,不管你使用Maven还是Gradle,这都能减少Spring工程构建文件的大小。

容器

在基于Spring的应用中,你的应用对象生存于Spring容器(container)中。

容器是Spring框架的核心。Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联。

Spring容器并不是只有一个。Spring自带了多个容器实现,可以归为两种不同的类型。

  • bean工厂(由org.springframework. beans. factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持。
  • 应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者。

虽然可以在bean工厂和应用上下文之间任选一种,但bean工厂对大多数应用来说往往太低级了,因此,应用上下文要比bean工厂更受欢迎。

应用上下文

Spring自带了多种类型的应用上下文。

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

bean的生命周期

bean在Spring容器中从创建到销毁经历了若干阶段,每一阶段都可以针对Spring如何管理bean进行个性化定制

bean-life-cycle

  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使用init-method声明了初始化方法,该方法也会被调用
  8. 如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法
  9. 此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用

IOC

使用new实例创建对象耦合度太高,使用工厂模式仅与工厂耦合,将创建对象的工作交给spring工厂完成

创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。

优势

  • 创建应用对象之间关联关系的传统方法(通过构造器或者查找)通常会导致结构复杂的代码,这些代码很难被复用也很难进行单元测试。如果情况不严重的话,这些对象所做的事情只是超出了它应该做的范围;而最坏的情况则是,这些对象彼此之间高度耦合,难以复用和测试。
  • 在Spring中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。

DI所带来的最大收益——松耦合。

耦合具有两面性

  • 一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性(修复一个bug,将会出现一个或者更多新的bug)。
  • 另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。

对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。

原理

  • XML配置文件
  • dom4j解析
  • 工厂设计模式
  • 反射

依赖控制反转的实现有很多种方式。在Spring中,IoC容器是实现这个模式的载体,它可以在对象生成或初始化时直接将数据注入到对象中, 也可以通过将对象引用注入到对象数据域中的方式来注入对方法调用的依赖。这种依赖注入是可以递归的,对象被逐层注入。 就此而言,这种方案有一种完整而简洁的美感,它把对象的依赖关系有序地建立起来,简化了对象依赖关系的管理,在很大程度上简化了面向对象系统的复杂性。

通过使用IoC容器,对象依赖关系的管理被反转了,转到IoC容器中来了,对象之间的相互依赖关系由IoC容器进行管理,并由Ioc容器完成对象的注入。 这样就在很大程度上简化了应用的开发,把应用从复杂的对象依赖关系管理中解放出来。

IoC容器系列的设计与实现:BeanFactory和ApplicationContext

概述

容器的基本工作原理,可以大致整理出以下几个方面:

  • BeanDefinition的定位
    • 对IoC容器来说,它为管理POJO之间的依赖关系提供了帮助,但也要依据Spring的定义规则提供Bean定义信息。我们可以使用各种形式的Bean定义信息,其中比较熟悉和常用的是使用XML的文件格式。在Bean定义方面,Spring为用户提供了很大的灵活性。在初始化IoC容器的过程中,首先需要定位到这些有效的Bean定义信息,这里Spring使用Resource接口来统一这些Bean定义信息,而这个定位由ResourceLoader来完成。如果使用上下文,ApplicationContext本身就为客户提供了定位的功能。因为上下文本身就是DefaultResourceLoader的子类。如果使用基本的BeanFactory作为IoC容器,客户需要做的额外工作就是为BeanFactory指定相应的Resource来完成Bean信息的定位。
  • 容器的初始化
    • 在使用上下文时,需要一个对它进行初始化的过程,完成初始化以后,这个IoC容器才是可用的。这个过程的入口是在refresh中实现的,这个refresh相当于容器的初始化函数。在初始化过程中,比较重要的部分是对BeanDefinition信息的载入和注册工作。相当于在IoC容器中需要建立一个BeanDefinition定义的数据映像,Spring为了达到载入的灵活性,把载入的功能从IoC容器中分离出来,由BeanDefinitionReader来完成Bean定义信息的读取、解析和IoC容器内部BeanDefinition的建立。在DefaultListableBeanFactory中,这些BeanDefinition被维护在一个Hashmap中,以后的IoC容器对Bean的管理和操作就是通过这些BeanDefinition来完成的。
  • 依赖注入
    • 在容器初始化完成以后,IoC容器的使用就准备好了,但这时只是在IoC容器内部建立了BeanDefinition,具体的依赖关系还没有注入。在客户第一次向IoC容器请求Bean时,IoC容器对相关的Bean依赖关系进行注入。如果需要提前注入,客户可以通过lazy-init属性进行预实例化,这个预实例化是上下文初始化的一部分,起到提前完成依赖注入的控制作用。在依赖注入完成以后,IoC容器就会保持这些具备依赖关系的Bean供客户直接使用。这时可以通过getBean来取得Bean,这些Bean不是简单的Java对象,而是已经包含了对象之间依赖关系的Bean,尽管这些依赖注入的过程对用户来说是不可见的。
接口定义和继承结构

在Spring IoC容器的设计中,我们可以看到两个主要的容器系列:

一个是实现BeanFactory接口的简单容器系列,这系列容器只实现了容器的最基本功能 另一个是ApplicationContext应用上下文,它作为容器的高级形态而存在。应用上下文在简单容器的基础上,增加了许多面向框架的特性,同时对应用环境作了许多适配

IoC容器的接口设计图:

ioc-interface

  • 从接口BeanFactory到HierarchicalBeanFactory,再到ConfigurableBeanFactory,是一条主要的BeanFactory设计路径。
    • BeanFactory接口定义了基本的IoC容器的规范。在这个接口定义中,包括了getBean()这样的IoC容器的基本方法(通过这个方法可以从容器中取得Bean)
    • HierarchicalBeanFactory接口在继承了BeanFactory的基本接口之后,增加了getParentBeanFactory()的接口功能,使BeanFactory具备了双亲IoC容器的管理功能
    • ConfigurableBeanFactory接口中,主要定义了一些对BeanFactory的配置功能,比如通过setParentBeanFactory()设置双亲IoC容器,通过addBeanPostProcessor()配置Bean后置处理器,等等
    • 通过这些接口设计的叠加,定义了BeanFactory就是简单IoC容器的基本功能
  • 第二条接口设计主线是,以ApplicationContext应用上下文接口为核心的接口设计,这里涉及的主要接口设计有,从BeanFactory到ListableBeanFactory,再到ApplicationContext,再到我们常用的WebApplicationContext或者ConfigurableApplicationContext接口
    • 常用的应用上下文基本上都是ConfigurableApplicationContext或者WebApplicationContext的实现
    • 在这个接口体系中,ListableBeanFactory和HierarchicalBeanFactory两个接口,连接BeanFactory接口定义和ApplicationConext应用上下文的接口定义
    • 在ListableBeanFactory接口中,细化了许多BeanFactory的接口功能,比如定义了getBeanDefinitionNames()接口方法
    • 对于ApplicationContext接口,它通过继承MessageSource、ResourceLoader、ApplicationEventPublisher接口,在BeanFactory简单IoC容器的基础上添加了许多对高级容器的特性的支持

注意事项:高版本中ApplicationContext接口不在继承AutowireCapableBeanFactory接口

applicationcontext-hierarchy

BeanFactory的定义,可以执行以下操作:

  • 通过接口方法containsBean让用户能够判断容器是否含有指定名字的Bean
  • 通过接口方法isSingleton来查询指定名字的Bean是否是Singleton类型的Bean。对于Singleton属性,用户可以在BeanDefinition中指定
  • 通过接口方法isPrototype来查询指定名字的Bean是否是prototype类型的。与Singleton属性一样,这个属性也可以由用户在BeanDefinition中指定
  • 通过接口方法isTypeMatch来查询指定了名字的Bean的Class类型是否是特定的Class类型。这个Class类型可以由用户来指定
  • 通过接口方法getType来查询指定名字的Bean的Class类型
  • 通过接口方法getAliases来查询指定了名字的Bean的所有别名,这些别名都是用户在BeanDefinition中定义的

ApplicationContext提供了以下BeanFactory不具备的新特性:

  • 支持不同的信息源。我们看到ApplicationContext扩展了MessageSource接口,这些信息源的扩展功能可以支持国际化的实现,为开发多语言版本的应用提供服务
  • 访问资源。这一特性体现在对ResourceLoader和Resource的支持上,这样我们可以从不同地方得到Bean定义资源。这种抽象使用户程序可以灵活地定义Bean定义信息,尤其是从不同的I/O途径得到Bean定义信息。这在接口关系上看不出来,不过一般来说,具体ApplicationContext都是继承了DefaultResourceLoader的子类。因为DefaultResourceLoader是AbstractApplicationContext的基类
  • 支持应用事件。继承了接口ApplicationEventPublisher,从而在上下文中引入了事件机制。这些事件和Bean的生命周期的结合为Bean的管理提供了便利
  • 在ApplicationContext中提供的附加服务。这些服务使得基本IoC容器的功能更丰富。因为具备了这些丰富的附加功能,使得ApplicationContext与简单的BeanFactory相比,对它的使用是一种面向框架的使用风格,所以一般建议在开发应用时使用ApplicationContext作为IoC容器的基本形式
IoC容器的初始化过程

包括BeanDefinition的Resouce定位、载入和注册三个基本过程,如AbstractApplicationContext的refresh()方法

  • 第一个过程是Resource定位过程。
    • 这个Resource定位指的是BeanDefinition的资源定位,它由ResourceLoader通过统一的Resource接口来完成,这个Resource对各种形式的BeanDefinition的使用都提供了统一接口。对于这些BeanDefinition的存在形式,相信大家都不会感到陌生。比如,在文件系统中的Bean定义信息可以使用FileSystemResource来进行抽象;在类路径中的Bean定义信息可以使用前面提到的ClassPathResource来使用,等等。这个定位过程类似于容器寻找数据的过程,就像用水桶装水先要把水找到一样。
  • 第二个过程是BeanDefinition的载入。
    • 这个载入过程是把用户定义好的Bean表示成IoC容器内部的数据结构,而这个容器内部的数据结构就是BeanDefinition。下面介绍这个数据结构的详细定义。具体来说,这个BeanDefinition实际上就是POJO对象在IoC容器中的抽象,通过这个BeanDefinition定义的数据结构,使IoC容器能够方便地对POJO对象也就是Bean进行管理。
  • 第三个过程是向IoC容器注册这些BeanDefinition的过程。
    • 这个过程是通过调用BeanDefinitionRegistry接口的实现来完成的。这个注册过程把载入过程中解析得到的BeanDefinition向IoC容器进行注册。通过分析,我们可以看到,在IoC容器内部将BeanDefinition注入到一个HashMap中去,IoC容器就是通过这个HashMap来持有这些BeanDefinition数据的。
BeanDefinition的Resource定位

Resource并不能由DefaultListableBeanFactory直接使用,Spring通过BeanDefinitionReader来对这些信息进行处理。

FileSystemXmlApplicationContext的继承体系:

filesystemxmlapplicationcontext-hierarchy

FileSystemXmlApplicationContext已经通过继承AbstractApplicationContext具备了ResourceLoader读入以Resource定义的BeanDefinition的能力,因为AbstractApplicationContext的基类是DefaultResourceLoader。

getResourceByPath的调用过程:

getResourceByPath

BeanDefinition的载入和解析

对IoC容器来说,这个载入过程,相当于把定义的BeanDefinition在IoC容器中转化成一个Spring内部表示的数据结构的过程。IoC容器对Bean的管理和依赖注入功能的实现,是通过对其持有的BeanDefinition进行各种相关操作来完成的。这些BeanDefinition数据在IoC容器中通过一个HashMap来保持和维护。

BeanDefinition载入中的交互过程:

bean-definition

BeanDefinition的载入分成两部分,首先通过调用XML的解析器得到document对象,但这些document对象并没有按照Spring的Bean规则进行解析。 在完成通用的XML解析以后,才是按照Spring的Bean规则进行解析的地方,这个按照Spring的Bean规则进行解析的过程是在documentReader中实现的。
处理的结果由BeanDefinitionHolder对象来持有。这个BeanDefinitionHolder除了持有BeanDefinition对象外,还持有其他与BeanDefinition的使用相关的信息,比如Bean的名字、别名集合等。
具体的Spring BeanDefinition的解析是在BeanDefinitionParserDelegate中完成的。

BeanDefinition在IoC容器中的注册

注册的调用过程:

bean-register

完成了BeanDefinition的注册,就完成了IoC容器的初始化过程。此时,在使用的IoC容器DefaultListableBeanFactory中已经建立了整个Bean的配置信息,而且这些BeanDefinition已经可以被容器使用了,它们都在beanDefinitionMap里被检索和使用。容器的作用就是对这些信息进行处理和维护。这些信息是容器建立依赖反转的基础,

IoC容器的依赖注入

依赖注入的过程:

DI-process

在Bean的创建和对象依赖注入的过程中,需要依据BeanDefinition中的信息来递归地完成依赖注入。这些递归都是以getBean为入口的。

一个递归是在上下文体系中查找需要的Bean和创建Bean的递归调用; 另一个递归是在依赖注入时,通过递归调用容器的getBean方法,得到当前Bean的依赖Bean,同时也触发对依赖Bean的创建和注入。

在对Bean的属性进行依赖注入时,解析的过程也是一个递归的过程。这样,根据依赖关系,一层一层地完成Bean的创建和注入,直到最后完成当前Bean的创建。有了这个顶层Bean的创建和对它的属性依赖注入的完成,意味着和当前Bean相关的整个依赖链的注入也完成了。

注意事项:在新版本的spring中,增加了AbstractNestablePropertyAccessor处理原来由BeanWrapperImpl处理的setPropertyValue,使用processKeyedProperty方法和processLocalProperty对原有的setPropertyValue方法进行了拆分,并对processLocalProperty中原有的反射调用交由PropertyHandler处理

容器其他相关特性
ApplicationContext和Bean的初始化及销毁

对ApplicationContext启动的过程是在AbstractApplicationContext中实现的。在使用应用上下文时需要做一些准备工作,这些准备工作在prepareBeanFactory()方法中实现。在这个方法中,为容器配置了ClassLoader、PropertyEditor和BeanPost-Processor等,从而为容器的启动做好了必要的准备工作。

容器初始化和关闭过程:

applicationcontext-start-close

在容器要关闭时,也需要完成一系列的工作,这些工作在doClose()方法中完成。在这个方法中,先发出容器关闭的信号,然后将Bean逐个关闭,最后关闭容器自身。

容器的实现是通过IoC管理Bean的生命周期来实现的。Spring IoC容器在对Bean的生命周期进行管理时提供了Bean生命周期各个时间点的回调。

  • Bean实例的创建
  • 为Bean实例设置属性
  • 调用Bean的初始化方法
    • Bean的初始化方法调用是在以下的initializeBean方法中实现的
  • 应用可以通过IoC容器使用Bean
  • 当容器关闭时,调用Bean的销毁方法

在调用Bean的初始化方法之前,会调用一系列的aware接口实现,把相关的BeanName、BeanClassLoader,以及BeanFactoy注入到Bean中去。

接着会看到对invokeInitMethods的调用,这时还会看到启动afterPropertiesSet的过程,当然,这需要Bean实现InitializingBean的接口,对应的初始化处理可以在InitializingBean接口的afterPropertiesSet方法中实现,这里同样是对Bean的一个回调。

最后,还会看到判断Bean是否配置有initMethod,如果有,那么通过invokeCustom-InitMethod方法来直接调用,最终完成Bean的初始化。

DisposableBeanAdapter类中可以看到destroy方法的实现。对Bean的销毁过程,首先对postProcessBeforeDestruction进行调用,然后调用Bean的destroy方法,最后是对Bean的自定义销毁方法的调用,整个过程和前面的初始化过程很类似。

lazy-init属性和预实例化

在finishBeanFactoryInitialization的方法中,封装了对lazy-init属性的处理,实际的处理是在DefaultListableBeanFactory这个基本容器的preInstantiateSingletons方法中完成的。

该方法对单件Bean完成预实例化,这个预实例化的完成巧妙地委托给容器来实现。如果需要预实例化,那么就直接在这里采用getBean去触发依赖注入,与正常依赖注入的触发相比,只有触发的时间和场合不同。

FactoryBean的实现

FactoryBean的生产特性是在getBean中起作用的

bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);

在getObjectForBeanInstance的实现方法中可以看到在FactoryBean中常见的getObject方法的接口,这里返回的已经是作为工厂的FactoryBean生产的产品,而不是FactoryBean本身。

这种FactoryBean的机制可以提供一个很好的封装机制,比如封装Proxy、RMI、JNDI等。

BeanPostProcessor的实现

BeanPostProcessor是使用IoC容器时经常会遇到的一个特性,这个Bean的后置处理器是一个监听器,它可以监听容器触发的事件。将它向IoC容器注册后,容器中管理的Bean具备了接收IoC容器事件回调的能力。

以getBean方法为起始的调用过程:

beanpostprocessor

applyBeanPostProcessorsBeforeInitialization和applyBeanPostProcessorsAfterInitialization方法分别执行BeanPostProcessor中的两个方法

autowiring(自动依赖装配)的实现

对autowirng属性进行处理,从而完成对Bean属性的自动依赖装配,是在populateBean中实现的。

// Add property values based on autowire by name if applicable.
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
    autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    autowireByType(beanName, mbd, bw, newPvs);
}
Bean的依赖检查

在Spring IoC容器中,设计了一个依赖检查特性,通过它,Spring可以帮助应用检查是否所有的属性都已经被正确设置。应用只需要在Bean定义中设置dependency-check属性来指定依赖检查模式即可,这里可以将属性设置为none、simple、object、all四种模式,默认的模式是none。具体的实现代码是在AbstractAutowireCapableBeanFactory实现createBean的过程populateBean中完成的。

Bean对IoC容器的感知

它是通过特定的aware接口来完成的。aware接口有以下这些:

  • BeanNameAware,可以在Bean中得到它在IoC容器中的Bean实例名称。
  • BeanFactoryAware,可以在Bean中得到Bean所在的IoC容器,从而直接在Bean中使用IoC容器的服务。
  • ApplicationContextAware,可以在Bean中得到Bean所在的应用上下文,从而直接在Bean中使用应用上下文的服务。
  • MessageSourceAware,在Bean中可以得到消息源。
  • ApplicationEventPublisherAware,在Bean中可以得到应用上下文的事件发布器,从而可以在Bean中发布应用上下文的事件。
  • ResourceLoaderAware,在Bean中可以得到ResourceLoader,从而在Bean中使用ResourceLoader加载外部对应的Resource资源。

在设置Bean的属性之后,调用初始化回调方法之前,Spring会调用aware接口中的setter方法。

作为依赖注入的一部分,postProcessBeforeInitialization会在initializeBean的实现过程中被调用,从而实现对aware接口的相关注入。

bean的实例化方式

  • 使用类无参构造方法创建
  • 使用静态工厂创建
  • 使用实例工厂创建
<!--通过无参构造-->
<bean id="myBean" class="com.xpress.model.MyBean"/>
<!--通过静态工厂-->
<bean id="myBean" class="com.xpress.factory.StaticBeanFactory" factory-method="getBean"/>
<!--通过实例工厂-->
<bean id="factory" class="com.xpress.factory.BeanFactory"/>
<bean id="myBean" factory-bean="factory" factory-method="getBean"/>

配置可选方案

三种主要的装配机制:

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

建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。

XML配置方式

使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext

配置文件加载
手动加载
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean myBean = (MyBean) applicationContext.getBean("myBean");
Web项目容器加载

实现思想:把加载配置文件和创建对象过程,在服务器启动的时候完成

实现原理:

  • ServletContext对象
  • 监听器
  1. 在服务器启动的时候,每个项目会创建一个ServletContext对象
  2. 在ServletContext对象创建的时候,使用监听器监听到ServletContext对象的创建,此时加载配置文件并创建对象
  3. 把创建出来的对象放到ServletContext域中(setAttribute()方法)
  4. 获取对象时,在ServletContext域中取出(getAttribute()方法)

引入spring-web.jar

<!-- 配置监听器 -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置文件位置 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>
配置文件

缺点:

  • 并没有JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。
  • Spring的XML配置并不能从编译期的类型检查中受益。

  • 配置文件:官方建议applicationContext.xml

dtd

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
    <!-- bean definitions here -->
    <!-- 引入其他配置 -->
    <import resource="cd-config.xml"/>
    <!-- 引入配置类或者启动组件扫描 -->
    <bean class="soundsystem.CDConfig"/>
</beans>

schema

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- bean definitions here -->
</beans>
bean标签
常用属性
  • id:名称,不能包含特殊符号
  • class:对象全路径
  • name:同id,可以包含特殊符号(兼容struts1,已废弃)
  • scope:作用域
    • singleton:单例的(默认)
    • prototype:多例的
    • 需要在web.xml中配置org.springframework.web.context.request.RequestContextListener支持
      • request:创建一个bean对象并存入request中,随request废弃而废弃
      • session:创建一个bean对象并存入session中,随session废弃而废弃
      • globalSession:应用在Portlet环境放到globalSession中,非Portlet环境相当于session,生命周期在portlet Session的生命周期范围
  • lazy-init:懒加载,第一次获取bean时才初始化,singleton时默认容器启动就初始化bean
    • 在beans标签可以设置默认懒加载default-lazy-init=”true”
  • init-method:初始化方法,创建实例后调用
  • destroy-method:销毁方法,在context正常关闭时调用
  • autowire:配置当前bean的属性按何种方式自动装配
    • no:默认值,手动装配
    • byType:根据属性类型匹配,发现多个抛异常,没找到为null
    • byName:根据属性名称匹配,没找到为null
    • constructor:同byType,如果在容器中没有找到与构造器参数类型一致的bean,则抛异常
    • autodetect:通过bean的内省机制决定使用constructor还是byType方式,如果发现默认构造器,使用byType方式
    • 在beans标签可以设置默认装配方式default-autowire=”byName”
属性注入

作为一个通用的规则,我倾向于对强依赖使用构造器注入,而对可选性的依赖使用属性注入。

注入方式

  • set方法注入(spring支持)
  • 有参构造注入(spring支持)
  • 接口注入(实现指定结口的方法)

set方法注入

需要注入的属性必须有相应的set方法

<bean id="myBean" class="com.xpress.model.MyBean">
    <property name="name" value="xpress"/>
</bean>
<!-- 声明service -->
<bean id="myService" class="com.xpress.service.MyService"/>
<bean id="myAction" class="com.xpress.action.MyAction">
    <!-- 注入service -->
    <property name="myService" ref="myService"/>
</bean>

复杂类型注入

  • 数组
  • list集合
  • map集合
  • properties集合
<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

util命名空间:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xmlns:util="http://www.springframework.org/schema/util"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/util 
    http://www.springframework.org/schema/util/spring-util.xsd">

  <bean id="compactDisc"
        class="soundsystem.properties.BlankDisc"
        p:title="Sgt. Pepper's Lonely Hearts Club Band"
        p:artist="The Beatles"
        p:tracks-ref="trackList" />

  <util:list id="trackList">  
    <value>Sgt. Pepper's Lonely Hearts Club Band</value>
    <value>With a Little Help from My Friends</value>
    <value>Lucy in the Sky with Diamonds</value>
  </util:list>
</beans>

Spring util-命名空间中的元素:

元 素 描 述
util:constant 引用某个类型的public static域,并将其暴露为bean
util:list 创建一个java.util.List类型的bean,其中包含值或引用
util:map 创建一个java.util.Map类型的bean,其中包含值或引用
util:properties 创建一个java.util.Properties类型的bean
util:property-path 引用一个bean的属性(或内嵌属性),并将其暴露为bean
util:set 创建一个java.util.Set类型的bean,其中包含值或引用

有参构造注入

constructor-arg:

 <bean id="myBean" class="com.xpress.model.MyBean">
    <!-- index可以省略 -->
    <constructor-arg index="0" name="name" value="xpress"/>
    <constructor-arg name="userDao" ref="userDao"/>
</bean>

c命名空间:

@Autowired
public CDPlayer(CompactDisc cd) {
    this.cd = this.cd;
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:c="http://www.springframework.org/schema/c"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="compactDisc" class="soundsystem.SgtPeppers" />
  <!-- 装配引用 -->
  <bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />
  <!-- 使用索引而不是变量名 -->
  <bean id="cdPlayer1" class="soundsystem.CDPlayer" c:_0-ref="compactDisc"/>
  <!-- 只有一个省略数字 -->
  <bean id="cdPlayer2" class="soundsystem.CDPlayer" c:_-ref="compactDisc"/>

  <!-- 装配值 -->
  <bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band"  c:_1="The Beatles" />
  <bean id="cdPlayer" class="soundsystem.CDPlayer" c:_0="compactDisc" />
</beans>

c-namespace

名称空间p注入

p-命名空间中属性所遵循的命名约定与c-命名空间中的属性类似。

<!-- 引入名称空间p -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myBean" class="com.xpress.model.MyBean" p:name="xpress"/>
<bean id="cdPlayer" class="soundsystem.properties.CDPlayer" p:compactDisc-ref="compactDisc" />

p-namespace

注解方式

Spring从两个角度来实现自动化装配:

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

对于基于Java的配置,Spring提供了AnnotationConfigApplicationContext

开启注解

引入context包schema,并打开注解扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启类,方法,属性上的注解扫描,完全使用注解-->
    <context:component-scan base-package="com.xpress.service,com.xpress.dao,com.xpress.model"/>
    <!--只开启属性上的注解扫描,仍然需要在配置文件中声明bean-->
    <!--<context:annotation-config/>-->
</beans>

如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。Spring将会扫描这个包以及这个包下的所有子包,查找带有注解的类,并且会在Spring中自动为其创建一个bean。

@Configuration
@ComponentScan// 以配置类所在的包作为基础包(base package)来扫描组件
// @ComponentScan(basePackageClasses = {CDPlayer.class,SgtPeppers.class}) // 这些类所在的包将会作为组件扫描的基础包
// @ComponentScan(basePackages={"soundsystem","video"}) // 更加清晰地表明你所设置的是基础包
public class CDPlayerConfig { 
}

annotation-config配置隐式注册了多个对注解进行解析的处理器

  • spring-beans:org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor 处理@Autowired注解
  • spring-beans:org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor 处理@Resource
  • spring-context:org.springframework.context.annotation.CommonAnnotationBeanPostProcessor 处理持久化相关注解
  • spring-tx:org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor 处理@Required注解
创建对象

目前提供的4个注解功能都是一样的,spring为了以后对注解功能进行拓展

  • @Component 泛指组件
  • @Controller WEB层
  • @Service 业务层
  • @Repository 持久层
@Component(value = "myBean")// <bean id="myBean"
@Scope(value = "singleton")// 指定单例,不指定默认单例
public class MyBean {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    // 注解指定init-method,在注入属性后执行
    @PostConstruct
    public void init(){}
    @PreDestroy
    public void destroy() {}
}
注入属性

注解可以用于属性上和属性的set方法上

  • @Autowired
    • 默认按类型进行装配,设置@Qualifier指定按名称装配
    • 默认要求必须存在,设置required属性为false允许null
    • 可以用在set方法上、属性上、构造器上和其他方法上
  • @Resource(name = “userDao”)
    • 默认按名称进行装配,找不到匹配名称才使用类型装配
    • name属性可以指定名称,指定名称后匹配不到将不会按类型装配
  • @Inject
@Service("userService")
public class UserService {
    @Resource(name = "userDao")
    private UserDao userDao;
    @Autowired
    @Qualifier("userDao")// 指定按名称进行装配
    private UserDao userDao;
}
@Component
public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;
  @Autowired // 能够用在构造器上
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }
  public void play() {
    cd.play();
  }
}

JavaConfig

在进行显式配置时,JavaConfig是更好的方案,因为它更为强大、类型安全并且对重构友好。

它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其他的组件一样都使用相同的语言进行表述,但JavaConfig是配置代码。这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。

  • 默认情况下,bean的ID与带有@Bean注解的方法名是一样的,也可以通过name属性指定一个不同的名字
  • 方法上添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。
@Configuration
@Import({CDPlayerConfig.class,CDConfig.class})// 引入其他配置
@ImportResource("classpath:cd-config.xml")// 引入xml配置
public class CDPlayerConfig {
    @Bean//(name = "compactDisc")
    public CompactDisc compactDisc() {
        return new SgtPeppers();
    }
    // 自动装配合适的compactDisc
    // 你可以将配置分散到多个配置类、XML文件以及自动扫描和装配bean之中,只要功能完整健全即可。
    @Bean
    // @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE //控制实例为单例还是多例
    public CDPlayer cdPlayer(CompactDisc compactDisc) {
        return new CDPlayer(compactDisc);
    }
}
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {
    @Autowired
    private MediaPlayer player;
}

profile

JavaConfig

在Spring 3.1中,只能在类级别上使用@Profile注解。从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。

@Configuration
public class DataSourceConfig {
    @Bean
    @Profile("dev")
    public DataSource embeddedDataSource() {
      return new EmbeddedDatabaseBuilder()
          .setType(EmbeddedDatabaseType.H2)
          .addScript("classpath:schema.sql")
          .addScript("classpath:test-data.sql")
          .build();
    }
    @Bean
    @Profile("prod")
    public DataSource jndiDataSource() {
      JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
      jndiObjectFactoryBean.setJndiName("jdbc/myDS");
      jndiObjectFactoryBean.setResourceRef(true);
      jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
      return (DataSource) jndiObjectFactoryBean.getObject();
  }

}

尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在一个给定的profile范围内。
没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。

XML

  • 可以通过<beans>元素的profile属性,在XML中配置profile bean。
  • 还可以在根<beans>元素中嵌套定义<beans>元素,而不是为每个环境都创建一个profile XML文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd" profile="dev">
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
    http://www.springframework.org/schema/jee
    http://www.springframework.org/schema/jee/spring-jee.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">
  <beans profile="dev">
    <jdbc:embedded-database id="dataSource" type="H2">
      <jdbc:script location="classpath:schema.sql" />
      <jdbc:script location="classpath:test-data.sql" />
    </jdbc:embedded-database>
  </beans>
  <beans profile="prod">
    <jee:jndi-lookup id="dataSource"
      lazy-init="true"
      jndi-name="jdbc/myDatabase"
      resource-ref="true"
      proxy-interface="javax.sql.DataSource" />
  </beans>
</beans>

激活profile

Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:

  • spring.profiles.active
  • spring.profiles.default

如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的。但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。如果均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在profile中的bean。

有多种方式来设置这两个属性:

  • 作为DispatcherServlet的初始化参数;
  • 作为Web应用的上下文参数;
  • 作为JNDI条目;
  • 作为环境变量;
  • 作为JVM的系统属性;
  • 在集成测试类上,使用@ActiveProfiles注解设置。

Tips:一种方式是使用DispatcherServlet的参数将spring.profiles.default设置为开发环境的profile,当应用程序部署到QA、生产或其他环境之中时,负责部署的人根据情况使用系统属性、环境变量或JNDI设置spring.profiles.active即可。

web.xml

<!-- 为上下文设值默认的profile -->
<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>dev</param-value>
</context-param>

<servlet>
    <servlet-name>spring-webmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <!--为 Servlet设值默认profile -->
        <param-name>spring.profiles.default</param-name>
        <param-value>dev</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

测试


public class DataSourceConfigTest {
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = DataSourceConfig.class)
    @ActiveProfiles("prod")// 指定运行测试时要激活哪个profile
    public static class ProductionDataSourceTest {
        @Autowired
        private DataSource dataSource;
        @Test
        public void shouldBeEmbeddedDatasource() {
            // should be null, because there isn't a datasource configured in JNDI
            assertNull(dataSource);
        }
    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:datasource-config.xml")
    @ActiveProfiles("dev")
    public static class DevDataSourceTest_XMLConfig {
        @Autowired
        private DataSource dataSource;
        @Test
        public void shouldBeEmbeddedDatasource() {
            assertNotNull(dataSource);
            JdbcTemplate jdbc = new JdbcTemplate(dataSource);
            List<String> results = jdbc.query("select id, name from Things", new RowMapper<String>() {
                @Override
                public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                    return rs.getLong("id") + ":" + rs.getString("name");
                }
            });
            assertEquals(1, results.size());
            assertEquals("1:A", results.get(0));
        }
    }
}

条件化的bean

Spring 4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。

@Configuration
public class MagicConfig {
    @Bean
    @Conditional(MagicExistsCondition.class)
    public MagicBean magicBean() {
        return new MagicBean();
    }
}
public class MagicExistsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("magic");
    }
}

matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。

通过ConditionContext,可以做到如下几点:

  • 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义
  • 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性
  • 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么
  • 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源
  • 借助getClassLoader()返回的ClassLoader加载并检查类是否存在

通过AnnotatedTypeMetadata,可以做到如下几点:

  • 借助isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解
  • 借助其他的那些方法,能够检查@Bean注解的方法上其他注解的属性

处理自动装配的歧义性

可以将可选bean中的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。

标示首选的bean

Spring将会使用首选的bean,而不是其他可选的bean。

@Component
@Primary
public IceCream implements Dessert{}
@Bean
@Primary
public Dessert iceCream{
    return new IceCream();
}
<bean id="iceCream" class="com.dessert.IceCream" primary="true"/>

限定自动装配的bean

@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用
可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。在这里所需要做的就是在bean声明上添加@Qualifier注解。它可以与@Component组合使用

@Component
@Qualifier("dessert")
public IceCream implements Dessert{}
@Autowired
@Qualifier("dessert")
public void setDessert(Dessert dessert){
    this.dessert = dessert;
}

bean的作用域

Spring定义了多种作用域:

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

如果选择其他的作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.INTERFACES)
public class Notepad {
    // the details of this class are inconsequential to this example
}

proxyMode:

  • proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
  • 一个具体的类的话,Spring就没有办法创建基于接口的代理了,它使用CGLib来生成基于类的代理,必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理

XML的作用域参看上文XML配置中bean标签部分

运行时值注入

Spring提供了两种在运行时求值的方式:

  • 属性占位符(Property placeholder)。
  • Spring表达式语言(SpEL)。

注入外部的值

处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。

Environment
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class EnvironmentConfig {
    @Autowired
    Environment env;
    @Bean
    public BlankDisc blankDisc() {
        return new BlankDisc(
                env.getProperty("disc.title"),
                env.getProperty("disc.artist"));
    }
}

占位符

开启

<context:property-placeholder location="com/soundsystem/app.properties" />
@Configuration
public class EnvConfig {
    @Bean
    public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() {
        PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer();
        propertyPlaceholderConfigurer.setLocation(new ClassPathResource("/com/soundsystem/app.properties"));
        return propertyPlaceholderConfigurer;
    }
}

使用

<bean class="com.soundsystem.BlankDisc" c:_0 = "${disc.title}" c:_1 = "${disc.artist}"/>
public BlankDisc(@Value("${disc.title}") String title,
                 @Value("${disc.artist}") String artist) {
    this.title = title;
    this.artist = artist;
}

使用Spring表达式语言进行装配

Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),SpEL拥有很多特性,包括:

  • 使用bean的ID来引用bean
  • 调用方法和访问对象的属性
  • 对值进行算术、关系和逻辑运算
  • 正则表达式匹配
  • 集合操作

SpEL可以在@Value注解和xml配置中使用

SpEL通过ID引用其他的bean。

#{artistSelect.selectArtist()?.toUpperCase()}

使用了“?.”运算符。这个运算符能够在访问它右边的内容之前,确保它所对应的元素不是null。所以,如果selectArtist()的返回值是null的话,那么SpEL将不会调用toUpperCase()方法。表达式的返回值会是null。

要在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键的运算符。例

T(java.lang.Math).PI

运算符

运算符类型 运算符
算术运算 +、-、 * 、/、%、^
比较运算 < 、 > 、 == 、 <= 、 >= 、 lt 、 gt 、 eq 、 le 、 ge
逻辑运算 and 、 or 、 not 、
条件运算 ?: (ternary) 、 ?: (Elvis)
正则表达式 matches
#{2 * T(java.lang.Math).PI * circle.radius ^ 2}
#{scoreRecord.score > 1000 ? "Winner" : "Loser"}
// 三元运算符的一个常见场景就是检查null值,并用一个默认值来替代null。
#{disk.title ?: 'Rattle and Hum'}
#{user.email mathces '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}

计算集合

SpEL还提供了查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集

#{jukebox.songs.?[artist eq 'Joe']}

SpEL还提供了另外两个查询运算符:“.^[]”和“.$[]”,它们分别用来在集合中查询第一个匹配项和最后一个匹配项

#{jukebox.songs.^[artist eq 'Joe']}

SpEL还提供了投影运算符(.![]),它会从集合的每个成员中选择特定的属性放到另外一个集合中

#{jukebox.songs.![title]}

投影操作可以与其他任意的SpEL运算符一起使用

#{jukebox.songs.?[artist eq 'Joe'].![title]}

IOC和DI

  • IOC:控制反转,把对象创建交给spring配置
  • DI:依赖注入,向对象属性中设置值
  • 关系:DI不能单独存在,要在IOC基础上完成

AOP

面向切面编程,采取横向抽取机制,取代了传统纵向继承体系和委托重复性代码(性能监视、事务处理、安全检查、缓存)

实现原理

  • 针对有接口的情况,使用jdk的动态代理产生接口代理对象
  • 针对没有接口的情况,使用cglib产生子类代理对象

直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。

Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。 Spring缺少对字段连接点的支持,无法让创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,就无法在bean创建时应用通知。 但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么可以利用Aspect来补充Spring AOP的功能。

概述

AOP联盟定义的AOP体系结构:

aop-architecture

切点在Spring AOP中的类继承体系:

pointcut-hierarchy

AopProxy代理对象的建立

在Spring的AOP模块中,一个主要的部分是代理对象的生成,而对于Spring应用,是通过配置和调用Spring的ProxyFactoryBean来完成这个任务的。

ProxyFactory的设计为中心相关的类继承关系:

ProxyFactoryBean

  • 为共同基类,可以将ProxyConfig看成是一个数据基类,这个数据基类为ProxyFactoryBean这样的子类提供了配置属性
  • 在另一个基类AdvisedSupport的实现中,封装了AOP对通知和通知器的相关操作,这些操作对于不同的AOP的代理对象的生成都是一样的,但对于具体的AOP代理对象的创建,AdvisedSupport把它交给它的子类们去完成
  • 对于ProxyCreatorSupport,可以将它看成是其子类创建AOP代理对象的一个辅助类

通过继承以上提到的基类的功能实现,具体的AOP代理对象的生成,根据不同的需要,分别由ProxyFactoryBean、AspectJProxyFactory和ProxyFactory来完成。

  • 对于需要使用AspectJ的AOP应用,AspectJProxyFactory起到集成Spring和AspectJ的作用
  • 对于使用Spring AOP的应用,ProxyFactoryBean和ProxyFactoy都提供了AOP功能的封装
    • 使用ProxyFactoryBean,可以在IoC容器中完成声明式配置
    • 使用ProxyFactory,则需要编程式地使用Spring AOP的功能

AopProxy的生成过程:

aop-proxy

对具体的实现层次的代理对象的生成,是由Spring封装的JdkDynamicAopProxy和CglibProxyFactory类来完成的。

AopProxy接口的层次关系:

aopproxy-interface

Spring AOP拦截器调用的实现

JdkDynamicAopProxy中的invoke方法,以及在CglibAopProxy中使用DynamicAdvisedInterceptor的intercept方法。 它们对拦截器链的调用都是在ReflectiveMethodInvocation中通过proceed方法实现的。

ProxyFactoryBean实现了BeanFactoryAware接口,并在initializeAdvisorChain方法中通过BeanFactory实例化时注入的interceptorNames向自己的BeanFactory调用getBean方法获取通知器

DefaultAdvisorChainFactory中方法getInterceptorsAndDynamicInterceptionAdvice通过调用GlobalAdvisorAdapterRegistry的getInterceptors对Advisor中的Advise进行了AdvisorAdapter转换

AdvisorAdapter接口中类的设计层次和关系:

advisoradapter

操作术语

  • Joinpoint:连接点,被拦截到的点,在spring中,指的是方法,spring只支持方法类型的拦截点
  • Pointcut:切入点,要对哪些连接点进行拦截的定义
  • Advice:通知/增强,拦截到连接点所要做的事
    • 前置通知:在方法之前执行
    • 后置通知:在方法之后执行
    • 异常通知:方法出现异常执行
    • 最终通知:后置之后执行
    • 环绕通知:在方法之前和之后执行
  • Aspect:切面,是切入点和通知的结合的过程
  • Introduction:引介,在运行期动态的添加的方法和Field
  • Target:目标对象,代理的目标对象
  • Weaving:织入,把增强应用到目标的过程
  • Proxy:代理,结果代理类

定义切面

Spring借助AspectJ的切点表达式语言来定义Spring切面:

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点

aspectj-perform

aspectj-within

使用了“&&”操作符形成与(and)关系,可以使用“||”操作符来标识或(or)关系,而使用“!”操作符来标识非(not)操作。
因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。

Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。

execution(* com.xpress.service.*.do(..) and !bean(excludeBeanId))

execution常用写法

execution(<访问修饰符>?<返回类型><方法名>(<参数>)<异常>)

  • 匹配所有public方法:execution(public * *(..))
  • 匹配指定包下所有类方法,不包含子包:execution(* com.xpress.dao.*(..))
  • 匹配指定包下所有类方法,包含子包:execution(* com.xpress.dao..*(..))
  • 匹配指定类的所有方法:execution(* com.xpress.dao.UserService.*(..))
  • 匹配实现特定接口所有类方法:execution(* com.xpress.dao.UserService+.*(..))
  • 匹配所有save开头方法:execution(* save*(..))
  • 匹配所有方法:execution(* .(..))
  • 匹配返回值:execution(java.lang.String .(..)) or execution(!void .(..))
  • 匹配参数:execution(* .(java.lang.String,..))

AOP操作

Spring 2.0以后AOP使用AspectJ实现,AspectJ是一个切面框架,它有一个专门的编译器生成遵循java字节编码规范的class文件

  • 基于AspectJ的xml方式
  • 基于AspectJ的注解方式

基于AspectJ的xml方式

配置

Spring的AOP配置元素能够以非侵入性的方式声明切面:

AOP配置元素 用
<aop:advisor> 定义AOP通知器
<aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功)
<aop:after-returning> 定义AOP返回通知
<aop:after-throwing> 定义AOP异常通知
<aop:around> 定义AOP环绕通知
<aop:aspect> 定义一个切面
<aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面
<aop:before> 定义一个AOP前置通知
<aop:config> 顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内

引入aop schema

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- bean definitions here -->
<!-- 定义advice,实现标记接口org.aopalliance.aop.Advice -->
<bean name="myAdvice" class="com.xpress.advice.MyAdvice"/>
<aop:config>
    <!-- 配置切入点 -->
    <aop:pointcut id="myPointcut"
                  expression="execution(* com.xpress.dao..*(..)) or execution(* com.xpress.dao..*(..))"/>
    <!-- 配置切面 -->
    <aop:aspect ref="myAdvice">
        <!-- 指定切入点和增强的方法 -->
        <aop:before method="before" pointcut-ref="myPointcut"/>
        <!-- 返回之后执行,是否有异常都执行 -->
        <aop:after method="after" pointcut-ref="myPointcut"/>
        <!-- 返回之后执行 -->
        <aop:after-returning method="afterReturn" pointcut-ref="myPointcut"/>
        <!-- 环绕执行 -->
        <aop:around method="around" pointcut-ref="myPointcut"/>
        <!-- 抛出异常后执行 -->
        <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
    </aop:aspect>
    <!-- 参数处理 -->
    <bean id="trackCounter" class="com.xpress.aspect.TrackCounter"/>
    <aop:aspect ref="trackCounter">
        <aop:pointcut id="trackPlayed" expression="execution(* com.xpress.CompactDisc.playTrack(list) and args(trackNumber))"/>
        <!-- 指定切入点和增强的方法 -->
        <aop:after method="countTrack" pointcut-ref="trackPlayed"/>
    </aop:aspect>
    <!-- 加入新功能 -->
    <aop:aspect>
        <aop:declare-parents types-matching="com.xpress.Performance+" implement-interface="com.xpress.Encoreable"
            default-impl="com.xpress.aspect.DefaultEncoreableImpl" />
        <!-- default-impl替代 -->
        <!-- delegate-ref="beanId" -->
    </aop:aspect>
</aop:config>
</beans>

xml-aop

执行顺序
  1. before方法
  2. around的target方法前部分
  3. target方法
  4. around的target方法后部分
  5. after-return方法
  6. after方法

基于AspectJ的注解方式

  • @Before
  • @AfterReturning
  • @Around
  • @AfterThrowing
  • @After

开启AspectJ注解

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- 开启注解 -->
    <aop:aspectj-autoproxy/>
    <!-- 声明target -->
    <bean id="userDao" class="com.xpress.dao.UserDao"/>
    <!-- 声明切面 -->
    <bean id="myAdvice" class="com.xpress.advice.MyAdvice"/>
</beans>
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class MyAdviceConfig {
    @Bean
    public MyAdvice myAdvice(){
        return new MyAdvice();
    }
}

编写切面

@Aspect
public class MyAdvice {
    @Before("execution(* com.xpress.dao..*(..)) || execution(* com.xpress.service..*(..))")
    public void before() {
        System.out.println("before..");
    }
}

pointcut形式

<context:component-scan base-package="com.xpress.service,com.xpress.dao,com.xpress.model,com.xpress.advice"/>
<aop:aspectj-autoproxy/>
@Aspect
@Component //加载类,全部使用注解形式
public class AnnounceAdvice {
    //声明一个切入点
    @Pointcut("execution(* com.xpress.service..*(..))")
    private void pointcut() {
    }
    @Before("pointcut() && args(name)")// 获取参数,只会匹配有参数的方法
    public void doAccessCheck(String name) {
        System.out.println("前置通知:" + name);
    }
    @AfterReturning(pointcut = "pointcut()", returning = "result")// 获取返回值,只会匹配有返回值的方法
    public void doAfterReturning(String result) {
        System.out.println("后置通知:" + result);
    }
    @After("pointcut()")
    public void doAfter() {
        System.out.println("最终通知");
    }
    @AfterThrowing(pointcut = "pointcut()", throwing = "e")// 获取异常
    public void doAfterThrowing(Exception e) {
        System.out.println("例外通知:" + e);
    }
    // 你可以不调用proceed()方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。
    // 要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。
    @Around("pointcut()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("进入方法");
        Object result = pjp.proceed();
        System.out.println("退出方法");
        return result;
    }
}

处理参数

public class Player {
    private List<String> musics;
    public void setMusics(List<String> musics) {
        this.musics = musics;
    }
    public void play(int index) {
        System.out.println("playing " + musics.get(index));
    }

}
@Aspect
public class PlayerCounter {
    private Map<Integer, Integer> count = new HashMap<>();
    public Map<Integer, Integer> getCount() {
        return count;
    }
    @Pointcut("execution(* com.xpress.Player..*(int)) && args(index)")
    public void playCount(int index) {
    }
    @After(value = "playCount(index)", argNames = "index")
    public void saveCount(int index) {
        Integer value = 1;
        if (count.containsKey(index)) {
            value = count.get(index) + 1;
        }
        count.put(index, value);
    }
}
@Configuration
@EnableAspectJAutoProxy
public class PlayerConfig {
    @Bean
    public Player player() {
        ArrayList<String> musics = new ArrayList<>();
        musics.add("music 0");
        musics.add("music 1");
        musics.add("music 2");
        musics.add("music 3");
        musics.add("music 4");
        musics.add("music 5");
        Player player = new Player();
        player.setMusics(musics);
        return player;
    }
    @Bean
    public PlayerCounter playerCounter() {
        return new PlayerCounter();
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PlayerConfig.class})
public class AspectjTest {
    @Autowired
    private Player player;
    @Autowired
    private PlayerCounter playerCounter;
    @Test
    public void testAspectJ() {
        player.play(1);
        player.play(1);
        player.play(2);
        player.play(3);
        player.play(5);
        player.play(2);
        System.out.println(playerCounter.getCount());
    }
}

功能增强

public interface Encoreable {
    void performEncore();
}
public class DefaultEncoreableImpl implements Encoreable {
    @Override
    public void performEncore() {
        System.out.println("perform....");
    }
}
@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value = "com.xpress.Player+", defaultImpl = DefaultEncoreableImpl.class)
    private Encoreable encoreable;
}
@Around(value = "playCount(index)", argNames = "pjp,index")
public Object testEncore(ProceedingJoinPoint pjp, int index) {
    if (pjp.getThis() instanceof Encoreable) {// 可以在类似地方判断代理类是个增强后的实现类
        ((Encoreable) pjp.getThis()).performEncore();
    }
    //...
}

@DeclareParents注解由三部分组成:

  • value属性指定了哪种类型的bean要引入该接口。在本例中,也就是所有实现Performance的类型。(标记符后面的+号表示是Performance的所有子类型,而不是Performance本身。)
  • defaultImpl属性指定了为引入功能提供实现的类。在这里,我们指定的是DefaultEncoreable提供实现。
  • @DeclareParents注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是Encoreable接口。

数据访问与集成

数据源配置

参看JDBC的数据源部分

jdbc.properties

<!-- 指定配置文件位置 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--datasource-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <!-- 连接池启动时的初始值 -->
    <property name="initialSize" value="${jdbc.initialSize}"/>
    <!-- 连接池的最大值 -->
    <property name="maxActive" value="${jdbc.maxActive}"/>
    <!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
    <property name="maxIdle" value="${jdbc.maxIdle}"/>
    <!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
    <property name="minIdle" value="${jdbc.minIdle}"/>
</bean>
<!--name-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="datasource"/>
</bean>
<!--dao-->
<bean id="userDao" class="com.xpress.dao.UserDao">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>

注入properties值

@Value("${redis.creative.tag.rel}")
private static String CREATIVE_TAG_PREFIX;
@Value("${redis.creative.classify.rel}")
private static String CREATIVE_CLASSIFY_PREFIX;

JNDI

jndi方式配置数据源交个容器管理,如tomcat的context.xml中配置连接池属性

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd 
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd"
       default-lazy-init="true">

    <jee:jndi-lookup id="datasourceJndi" jndi-name="tomcatJndi" resource-ref="true"/>
    <bean id="dataSourceJndi" class="org.springframework.jndi.JndiObjectFactoryBean">
        <!--关联datasourceJndi配置-->
        <property name="jndiName" ref="datasourceJndi"/>
        <!--通过name直接配置-->
        <property name="jndiName" value="tomcatJndi"/>
    </bean>
</beans>

ORM

随着应用程序变得越来越复杂,对持久化的需求也变得更复杂:

  • 延迟加载(Lazy loading):随着我们的对象关系变得越来越复杂,有时候我们并不希望立即获取完整的对象间关系。
  • 预先抓取(Eager fetching):这与延迟加载是相对的。借助于预先抓取,我们可以使用一个查询获取完整的关联对象。
  • 级联(Cascading):有时,更改数据库中的表会同时修改其他表。

Spring对ORM框架的支持提供了与这些框架的集成点以及一些附加的服务:

  • 支持集成Spring声明式事务
  • 透明的异常处理
  • 线程安全的、轻量级的模板类
  • DAO支持类
  • 资源管理

ORM的设计与实现

以Template为核心的类设计:

template

Hibernate的LocalSessionFactoryBean创建在InitializingBean接口的afterPropertiesSet方法调用时触发的

HibernateTemplate中execute的调用过程:

hibernate-template-execute

JdbcTemplate

参看《JDBC》文章内容

Hibernate

Spring3.1以后hibernate4包下,支持xml和注解

import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
@Bean
public SessionFactory sessionFactoryBean() {
    try {
        LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean();
        localSessionFactoryBean.setDataSource(dataSource());
        localSessionFactoryBean.setPackagesToScan("spittr.domain");
        Properties props = new Properties();
        props.setProperty("dialect", "org.hibernate.dialect.H2Dialect");
        localSessionFactoryBean.setHibernateProperties(props);
        localSessionFactoryBean.afterPropertiesSet();
        return localSessionFactoryBean.getObject();
    } catch (IOException e) {
        return null;
    }
}
@Repository
public class HibernateSpittleRepository implements SpittleRepository {
    private SessionFactory sessionFactory;
    @Inject
    public HibernateSpittleRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
    private Session currentSession() {
        return sessionFactory.getCurrentSession();//<co id="co_RetrieveCurrentSession"/>
    }
    public long count() {
        return findAll().size(); 
    }
    public List<Spittle> findRecent() {
        return findRecent(10);
    }
    public List<Spittle> findRecent(int count) {
        return (List<Spittle>) spittleCriteria()
                .setMaxResults(count)
                .list();
    }
    public Spittle findOne(long id) {
        return (Spittle) currentSession().get(Spittle.class, id);
    }
    public Spittle save(Spittle spittle) {
        Serializable id = currentSession().save(spittle);
        return new Spittle(
            (Long) id, 
            spittle.getSpitter(), 
            spittle.getMessage(), 
            spittle.getPostedTime());
    }
    public List<Spittle> findBySpitterId(long spitterId) {
        return spittleCriteria()
                .add(Restrictions.eq("spitter.id", spitterId))
                .list();
    }
    public void delete(long id) {
        currentSession().delete(findOne(id));
    }
    public List<Spittle> findAll() {
        return (List<Spittle>) spittleCriteria().list(); 
    }
    private Criteria spittleCriteria() {
        return currentSession() 
                .createCriteria(Spittle.class)
                .addOrder(Order.desc("postedTime"));
    }
}

PersistenceExceptionTranslationPostProcessor是一个bean 后置处理器(bean post-processor),它会在所有拥有@Repository注解的类上添加一个通知器(advisor),这样就会捕获任何平台相关的异常并以Spring非检查型数据访问异常的形式重新抛出。

@Bean
public BeanPostProcessor persistenceTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}

Java持久化API

Java持久化API(Java Persistence API,JPA)诞生在EJB 2实体Bean的废墟之上,并成为下一代Java持久化标准。JPA是基于POJO的持久化机制,它从Hibernate和Java数据对象(Java Data Object,JDO)上借鉴了很多理念并加入了Java 5注解的特性。

配置实体管理器工厂

基于JPA的应用程序需要使用EntityManagerFactory的实现类来获取EntityManager实例。JPA定义了两种类型的实体管理器:

  • 应用程序管理类型(Application-managed):当应用程序向实体管理器工厂直接请求实体管理器时,工厂会创建一个实体管理器。在这种模式下,程序要负责打开或关闭实体管理器并在事务中对其进行控制。这种方式的实体管理器适合于不运行在Java EE容器中的独立应用程序。
  • 容器管理类型(Container-managed):实体管理器由Java EE创建和管理。应用程序根本不与实体管理器工厂打交道。相反,实体管理器直接通过注入或JNDI来获取。容器负责配置实体管理器工厂。这种类型的实体管理器最适用于Java EE容器,在这种情况下会希望在persistence.xml指定的JPA配置之外保持一些自己对JPA的控制。

应用程序管理类型的EntityManager是由EntityManagerFactory创建的,

  • 应用程序管理类型的EntityManagerFactory是通过PersistenceProvider的createEntityManagerFactory()方法得到的
  • 与此相对,容器管理类型的EntityManagerFactory是通过PersistenceProvider的createContainerEntityManagerFactory()方法获得的

配置应用程序管理类型的JPA

对于应用程序管理类型的实体管理器工厂来说,它绝大部分配置信息来源于一个名为persistence.xml的配置文件。这个文件必须位于类路径下的META-INF目录下。

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="CasPersistence" transaction-type="RESOURCE_LOCAL">
        <class>org.jasig.cas.services.AbstractRegisteredService</class>
        <class>org.jasig.cas.services.RegexRegisteredService</class>
        <class>org.jasig.cas.services.RegisteredServiceImpl</class>
        <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>
        <class>org.jasig.cas.ticket.ServiceTicketImpl</class>
        <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>
    </persistence-unit>
    <persistence-unit name="spittrPU">
        <class>spittr.domain.Spitter</class>
        <properties>
            <property name="toplink.jdbc.driver" value="org.h2.Driver"/>
            <property name="toplink.jdbc.url" value="jdbc:h2:tcp://localhost/~/spittr"/>
            <property name="toplink.jdbc.username" value="sa"/>
            <property name="toplink.jdbc.password" value="sa"/>
        </properties>
    </persistence-unit>
</persistence>
@Bean
public LocalEntityManagerFactoryBean entityManagerFactoryBean() {
    LocalEntityManagerFactoryBean localEntityManagerFactoryBean = new LocalEntityManagerFactoryBean();
    // 赋给persistenceUnitName属性的值就是persistence.xml中持久化单元的名称。
    localEntityManagerFactoryBean.setPersistenceUnitName("spittrPU");
    return localEntityManagerFactoryBean;
}

创建应用程序管理类型的EntityManagerFactory都是在persistence.xml中进行的,而这正是应用程序管理的本意。在应用程序管理的场景下(不考虑Spring时),完全由应用程序本身来负责获取EntityManagerFactory,这是通过JPA实现的PersistenceProvider做到的。如果每次请求EntityManagerFactory时都需要定义持久化单元,那代码将会迅速膨胀。通过将其配置在persistence.xml中,JPA就能够在这个特定的位置查找持久化单元定义了。

但借助于Spring对JPA的支持,我们不再需要直接处理PersistenceProvider了。

使用容器管理类型的JPA

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
    LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
    // 尽管数据源还可以在persistence.xml中进行配置,但是这个属性指定的数据源具有更高的优先级。
    entityManagerFactoryBean.setDataSource(dataSource);
    entityManagerFactoryBean.setPersistenceUnitName("spittrPU");
    entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
    entityManagerFactoryBean.setPackagesToScan("spittr.domain");// 配置包扫描
    return entityManagerFactoryBean;
    // 上面的配置可以取代persistence.xml的作用
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
    HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
    adapter.setDatabase(Database.H2);
    adapter.setShowSql(true);
    adapter.setGenerateDdl(false);
    adapter.setDatabasePlatform("org.hibernate.dialect.H2Dialect");
    return adapter;
}

jpaVendorAdapter属性用于指明所使用的是哪一个厂商的JPA实现。Spring提供了多个JPA厂商适配器:

  • EclipseLinkJpaVendorAdapter
  • HibernateJpaVendorAdapter
  • OpenJpaVendorAdapter
  • TopLinkJpaVendorAdapter(在Spring 3.1版本中,已经将其废弃了)

Hibernate的JPA适配器支持多种数据库,可以通过其database属性配置使用哪个数据库:

数据库平台 属性database的值
IBM DB2 DB2
Apache Derby DERBY
H2 H2
Hypersonic HSQL
Informix INFORMIX
MySQL MYSQL
Oracle ORACLE
PostgresQL POSTGRESQL
Microsoft SQL Server SQLSERVER
Sybase SYBASE

从JNDI获取实体管理器工厂

<jee:jndi-lookup jndi-name="persistence/spittrPU" id="entityManagerFactoryBean"/>
@Bean
JndiObjectFactoryBean entityManagerFactoryBean() {
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    jndiObjectFactoryBean.setJndiName("persistence/spittrPU");
    return jndiObjectFactoryBean;
}
基于JPA的Repository
@Repository
@Transactional
public class JpaSpittrRepository {
    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;
    public void addSpittr(Spitter spitter) {
        entityManagerFactory.createEntityManager().persist(spitter);
    }
    public Spitter getSpittrById(long id) {
        return entityManagerFactory.createEntityManager().find(Spitter.class, id);
    }
    public void saveSpittr(Spitter spitter) {
        entityManagerFactory.createEntityManager().merge(spitter);
    }
}

问题在于每个方法都会调用createEntityManager()。除了引入易出错的重复代码以外,这还意味着每次调用Repository的方法时,都会创建一个新的EntityManager。这种复杂性源于事务。

这里的问题在于EntityManager并不是线程安全的,一般来讲并不适合注入到像Repository这样共享的单例bean中。但是,这并不意味着我们没有办法要求注入EntityManager。

@Repository
@Transactional
public class JpaSpitterRepository implements SpitterRepository {
    @PersistenceContext
    private EntityManager entityManager;
    public long count() {
        return findAll().size();
    }
    public Spitter save(Spitter spitter) {
        entityManager.persist(spitter);
        return spitter;
    }
    public Spitter findOne(long id) {
        return entityManager.find(Spitter.class, id);
    }
    public Spitter findByUsername(String username) {        
        return (Spitter) entityManager.createQuery("select s from Spitter s where s.username=?").setParameter(1, username).getSingleResult();
    }
    public List<Spitter> findAll() {
        return (List<Spitter>) entityManager.createQuery("select s from Spitter s").getResultList();
    }
}

@PersistenceContext并不会真正注入EntityManager——至少,精确来讲不是这样的。它没有将真正的EntityManager设置给Repository,而是给了它一个EntityManager的代理。真正的EntityManager是与当前事务相关联的那一个,如果不存在这样的EntityManager的话,就会创建一个新的。这样的话,我们就能始终以线程安全的方式使用实体管理器。

@PersistenceUnit和@PersistenceContext并不是Spring的注解,它们是由JPA规范提供的。为了让Spring理解这些注解,并注入EntityManager Factory或EntityManager,我们必须要配置Spring的Persistence-AnnotationBeanPostProcessor。如果你已经使用了<context:annotation-config><context:component-scan>,那么你就不必再担心了,因为这些配置元素会自动注册PersistenceAnnotationBeanPostProcessor bean。否则的话,我们需要显式地注册这个bean:

@Bean
public BeanPostProcessor persistenceTranslation() {
    return new PersistenceAnnotationBeanPostProcessor();
}

借助Spring Data实现自动化的JPA Repository

@Configuration
@EnableJpaRepositories(basePackages = "spittr.db")
public class JpaConfig {
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
       <!-- 扫描带有Component注解的类 -->
    <jpa:repositories base-package="spittr.db"/>
</beans>
public interface SpittleRepository extends JpaRepository<Spittle, Long>{
}

SpitterRepository接口,它扩展自JpaRepository,而JpaRepository又扩展自Repository标记接口(虽然是间接的)。因此,SpitterRepository就传递性地扩展了Repository接口,也就是Repository扫描时所要查找的接口。当Spring Data找到它后,就会创建SpitterRepository的实现类,其中包含了继承自JpaRepository、PagingAndSortingRepository和CrudRepository的18个方法。

自定义查询方法
public interface SpittleRepository extends JpaRepository<Spittle, Long> {
    List<Spittle> findBySpitterId(long spitterId);
}

当创建Repository实现的时候,Spring Data会检查Repository接口的所有方法,解析方法的名称,并基于被持久化的对象来试图推测方法的目的。本质上,Spring Data定义了一组小型的领域特定语言(domain-specific language ,DSL),在这里,持久化的细节都是通过Repository方法的签名来描述的。

AutoJPA

  • Spring Data允许在方法名中使用四种动词:get、read、find和count。
    • 其中,动词get、read和find是同义的,这三个动词对应的Repository方法都会查询数据并返回对象。而动词count则会返回匹配对象的数量,而不是对象本身。
  • 对于大部分场景来说,主题会被省略掉。要查询的对象类型是通过如何参数化JpaRepository接口来确定的,而不是方法名称中的主题。
    • readByFirstnameOrLastName
  • 如果主题的名称以Distinct开头的话,那么在生成查询的时候会确保所返回结果集中不包含重复记录。
  • 在断言中,会有一个或多个限制结果的条件。
    • IsAfter、After、IsGreaterThan、GreaterThan
    • IsGreaterThanEqual、GreaterThanEqual
    • IsBefore、Before、IsLessThan、LessThan
    • IsLessThanEqual、LessThanEqual
    • IsBetween、Between
    • IsNull、Null
    • IsNotNull、NotNull
    • IsIn、In
    • IsNotIn、NotIn
    • IsStartingWith、StartingWith、StartsWith
    • IsEndingWith、EndingWith、EndsWith
    • IsContaining、Containing、Contains
    • IsLike、Like
    • IsNotLike、NotLike
    • IsTrue、True
    • IsFalse、False
    • Is、Equals
    • IsNot、Not
  • 要对比的属性值就是方法的参数。参数的名称是无关紧要的,但是它们的顺序必须要与方法名称中的操作符相匹配。
  • 要处理String类型的属性时,条件中可能还会包含IgnoringCase或IgnoresCase,作为IgnoringCase/IgnoresCase的替代方案,我们还可以在所有条件的后面添加AllIgnoringCase或AllIgnoresCase
    • readByFirstnameOrLastNameIgnoresCase
    • readByFirstnameOrLastNameAllIgnoresCase
  • 我们还可以在方法名称的结尾处添加OrderBy,实现结果集排序。如果要根据多个属性排序的话,只需将其依序添加到OrderBy中即可。
    • readByFirstnameOrLastNameOrderByLastnameAsc
    • readByFirstnameOrLastNameOrderByLastnameAscFirstnameDesc
  • 条件部分是通过And或者Or进行分割的。
  • 其他符合规范的例子
    • findPetsByBreedIn(List breed)
    • countProductsByDiscontinuedTrue()
    • findByShippingDateBetween(Date start,Date end)
声明自定义查询(@Query)

@Query标注使用了HQL(Hibernate Query Language)进行查询

public interface SpittleRepository extends JpaRepository<Spittle, Long>, SpittleRepositoryCustom {
    @Query("select s from Spittle s where tittle like '%#{keywords}%'")
    List<Spittle> findByKeywords(String keywords);
}
public interface GenericDao<T> extends JpaRepository<T, ID> {
    @Query("select t from #{#entityName} t where t.id= ?1")
    public T findById(Long id);
}
混合自定义的功能

当Spring Data JPA为Repository接口生成实现的时候,它还会查找名字与接口相同,并且添加了Impl后缀的一个类。如果这个类存在的话,Spring Data JPA将会把它的方法与Spring Data JPA所生成的方法合并在一起。

public class SpittleRepositoryImpl implements SpittleRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;
    @Override
    public List<Spittle> findRecent() {
        return findRecent(10);
    }
    @Override
    public List<Spittle> findRecent(int count) {
        return (List<Spittle>) entityManager.createQuery("select s from Spittle s order by s.postedTime desc")
                .setMaxResults(count)
                .getResultList();
    }
}

我们还需要确保findRecent()方法会被声明在SpittleRepository接口中。要实现这一点,避免代码重复的简单方式就是修改SpittleRepository,让它扩展SpittleRepositoryCustom

Spring Data JPA将实现类与接口关联起来是基于接口的名称。但是,Impl后缀只是默认的做法,如果你想使用其他后缀的话,只需在配置@EnableJpaRepositories的时候,设置repositoryImplementationPostfix属性即可

@EnableJpaRepositories(basePackages = "spittr.db",repositoryImplementationPostfix = "Helper")

在XML中使用<jpa:repositories>元素来配置Spring Data JPA的话,我们可以借助repository-impl-postfix属性指定后缀:

<jpa:repositories base-package="spittr.db" repository-impl-postfix="Helper"/>

Redis

连接工厂

Spring Data Redis为四种Redis客户端实现提供了连接工厂:

  • JedisConnectionFactory
  • JredisConnectionFactory
  • LettuceConnectionFactory
  • SrpConnectionFactory
@Bean
public RedisConnectionFactory redisCF() {
    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
    jedisConnectionFactory.setHostName("127.0.0.1");
    jedisConnectionFactory.setPort(6379);
    jedisConnectionFactory.setPassword("53cr3t");
    return jedisConnectionFactory;
}
RedisTemplate

Spring Data Redis提供了两个模板:

  • RedisTemplate
  • StringRedisTemplate

RedisTemplate使用两个类型进行了参数化。第一个是key的类型,第二个是value的类型
如果所使用的value和key都是String类型,那么可以考虑使用StringRedisTemplate来代替RedisTemplate

@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
    redis.setConnectionFactory(cf);
    return redis;
}
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory cf) {
    return new StringRedisTemplate(cf);
}

RedisTemplate的很多功能是以子API的形式提供的,它们区分了单个值和集合值的场景:

方法 子API接口 描述
opsForValue() ValueOperations<K, V> 操作具有简单值的条目
opsForList() ListOperations<K, V> 操作具有list值的条目
opsForSet() SetOperations<K, V> 操作具有set值的条目
opsForZSet() ZSetOperations<K, V> 操作具有ZSet值(排序的set)的条目
opsForHash() HashOperations<K, HK, HV> 操作具有hash值的条目
boundValueOps(K) BoundValueOperations<K,V> 以绑定指定key的方式,操作具有简单值的条目
boundListOps(K) BoundListOperations<K,V> 以绑定指定key的方式,操作具有list值的条目
boundSetOps(K) BoundSetOperations<K,V> 以绑定指定key的方式,操作具有set值的条目
boundZSet(K) BoundZSetOperations<K,V> 以绑定指定key的方式,操作具有ZSet值(排序的set)的条目
boundHashOps(K) BoundHashOperations<K,V> 以绑定指定key的方式,操作具有hash值的条目
redisTemplate.opsForValue().set(product.getSku(), product);
Product found = redisTemplate.opsForValue().get(product.getSku());
redisTemplate.opsForList().rightPush("cart", product);
Product first = redisTemplate.opsForList().leftPop("cart");
Product last = redisTemplate.opsForList().rightPop("cart");
redisTemplate.opsForList().size("cart").longValue()
List<Product> products = redisTemplate.opsForList().range("cart", 2, 12);
redisTemplate.opsForSet().size("cart").longValue()
redisTemplate.opsForSet().add("cart1", product);
Set<Product> diff = redisTemplate.opsForSet().difference("cart1", "cart2");
Set<Product> union = redisTemplate.opsForSet().union("cart1", "cart2");
Set<Product> isect = redisTemplate.opsForSet().intersect("cart1", "cart2");
Product random = redisTemplate.opsForSet().randomMember("cart1");

绑定key操作

BoundListOperations<String, Product> cart = redisTemplate.boundListOps("cart");
cart.rightPush(product);
cart.rightPush(product2);
cart.rightPush(product3);

序列化器

当某个条目保存到Redis key-value存储的时候,key和value都会使用Redis的序列化器(serializer)进行序列化。Spring Data Redis提供了多个这样的序列化器,包括:

  • GenericToStringSerializer:使用Spring转换服务进行序列化
  • JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON
  • Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON
  • JdkSerializationRedisSerializer:使用Java序列化
  • OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化
  • StringRedisSerializer:序列化String类型的key和value

这些序列化器都实现了RedisSerializer接口,如果其中没有符合需求的序列化器,那么你还可以自行创建。

  • RedisTemplate会使用JdkSerializationRedisSerializer,这意味着key和value都会通过Java进行序列化。
  • StringRedisTemplate默认会使用StringRedis-Serializer

自定义序列化器:

@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redisTemplate = new RedisTemplate<String, Product>();
    redisTemplate.setConnectionFactory(cf);
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
    return redisTemplate;
}

@Test
public void settingKeyAndValueSerializers() {
    // need a local version so we can tweak the serializer
    RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
    redis.setConnectionFactory(cf);
    redis.setKeySerializer(new StringRedisSerializer());
    redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
    redis.afterPropertiesSet(); // if this were declared as a bean, you wouldn't have to do this
    // ...
}

事务管理

原理

在为事务处理配置好AOP的基础设施(比如,对应的Proxy代理对象和事务处理Interceptor拦截器对象)之后,首先需要完成对这些事务属性配置的读取,这些属性的读取处理是在TransactionInterceptor中实现的;在完成这些事务处理属性的读取之后,Spring为事务处理的具体实现做好了准备。
TransactionInterceptor,它是使用AOP实现声明式事务处理的拦截器,封装了Spring对声明式事务处理实现的基本过程;
还包括TransactionAttributeSource和TransactionAttribute这两个类,它们封装了对声明式事务处理属性的识别,以及信息的读入和配置。
在事务处理过程中,可以看到TransactionInfo和TransactionStatus这两个对象,它们是存放事务处理信息的主要数据对象,它们通过与线程的绑定来实现事务的隔离性。
在准备完这些与事务管理有关的数据之后,具体的事务处理是由事务处理器TransactionManager来完成的。在事务处理器完成事务处理的过程中,与具体事务处理器无关的操作都被封装到AbstractPlatformTransactionManager中实现了。
TransactionSynchronizationManager中使用ThreadLocal对事务控制的资源进行管理。

Spring的事务处理模块中的类层次结构:

Spring-tx-hierarchy

在Spring事务处理中,可以通过设计一个TransactionProxyFactoryBean来使用AOP功能,通过这个TransactionProxyFactoryBean可以生成Proxy代理对象,在这个代理对象中,通过TransactionInterceptor来完成对代理方法的拦截,正是这些AOP的拦截功能,将事务处理的功能编织进来。

声明式事务处理的实现大致可以分为以下几个部分:

  • 读取和处理在IoC容器中配置的事务处理属性,并转化为Spring事务处理需要的内部数据结构。
    • 具体来说,这里涉及的类是TransactionAttributeSourceAdvisor,从名字可以看出,它是一个AOP通知器,Spring使用这个通知器来完成对事务处理属性值的处理。处理的结果是,在IoC容器中配置的事务处理属性信息,会被读入并转化成TransactionAttribute表示的数据对象,这个数据对象是Spring对事物处理属性值的数据抽象,对这些属性的处理是和TransactionProxyFactoryBean拦截下来的事务方法的处理结合起来的。
  • Spring事务处理模块实现统一的事务处理过程。
    • 这个通用的事务处理过程包含处理事务配置属性,以及与线程绑定完成事务处理的过程,Spring通过TransactionInfo和TransactionStatus这两个数据对象,在事务处理过程中记录和传递相关执行场景。
  • 底层的事务处理实现。
    • 对于底层的事务操作,Spring委托给具体的事务处理器来完成,这些具体的事务处理器,就是在IoC容器中配置声明式事务处理时,配置的PlatformTransactionManager的具体实现,比如DataSourceTransactionManager和HibernateTransactionManager等。这两个具体的事务处理器的实现原理也是本章分析的内容。

在IoC容器进行注入的时候,会创建TransactionInterceptor对象,而这个对象会创建一个TransactionAttributePointcut,为读取TransactionAttribute做准备。在容器初始化的过程中,由于实现了InitializingBean接口,因此AbstractSingletonProxyFactoryBean会实现afterPropertiesSet()方法,正是在这个方法实例化了一个ProxyFactory,建立起Spring AOP的应用,在这里,会为这个ProxyFactory设置通知、目标对象,并最终返回Proxy代理对象。在Proxy代理对象建立起来以后,在调用其代理方法的时候,会调用相应的TransactionInterceptor拦截器,在这个调用中,会根据TransactionAttribute配置的事务属性进行配置,从而为事务处理做好准备。

建立事务处理对象的时序图:

transaction-proxy-factory-bean

在应用调用目标方法的时候,因为这个目标方法已经被TransactionProxyFactoryBean代理,所以TransactionProxyFactoryBean需要判断这个调用方法是否是事务方法。这个判断的实现,是通过在NameMatchTransactionAttributeSource中能否为这个调用方法返回事务属性来完成的。具体的实现过程是这样的:

  • 首先,以调用方法名为索引在nameMap中查找相应的事务处理属性值,如果能够找到,那么就说明该调用方法和事务方法是直接对应的;
  • 如果找不到,那么就会遍历整个nameMap,对保存在nameMap中的每一个方法名,使用PatternMatchUtils的SimpleMatch方法进行命名模式上的匹配。这里使用PatternMatchUtils进行匹配的原因是,在设置事务方法的时候,可以不需要为事务方法设置一个完整的方法名,而可以通过设置方法名的命名模式来完成,比如可以通过对通配符*的使用等。所以,如果直接通过方法名没能够匹配上,而通过方法名的命名模式能够匹配上,这个方法也是需要进行事务处理的方法,相对应地,它所配置的事务处理属性也会从nameMap中取出来,从而触发事务处理拦截器的拦截。

事务提交的时序图:

tx-commit

调用createTransactionIfNecessary的时序图:

creative-transaction-ifNecessary

实现DataSourceTransactionManager的时序图:

DataSourceTransactionManager

HibernateTransactionManager实现事务处理的时序图:

HibernateTransactionManager

TransactionManager

事务管理器 说明
org.springframework.jdbc.datasource.DataSourcetransactionManager 使用spring jdbc或ibatis进行持久化数据时
org.springframework.orm.hibernate5.HibernatetransactionManager 使用hibernate5.0版本进行持久化
org.springframework.orm.jpa.JpaTransactionManager 使用JPA进行持久化时使用
org.springframework.orm.jdo.JdoTransactionManager 持久化机制是Jdo时
org.springframework.transaction.jta.JtaTransactionManager 使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用

XML配置方式

<!--配置transactionManager事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="datasource"/>
</bean>
<!--配置advice-->
<tx:advice id="myTxAdvice">
    <tx:attributes>
        <tx:method name="get*" propagation="SUPPORTS"/>
        <tx:method name="delete*" propagation="REQUIRED"/>
        <tx:method name="update*" propagation="REQUIRED"/>
        <tx:method name="add*" propagation="REQUIRED"/>
        <tx:method name="*_FOR_NEW_TRANSACTION" propagation="REQUIRES_NEW"/>
    </tx:attributes>
</tx:advice>
<!--配置切面-->
<aop:config>
    <!-- 配置切入点 -->
    <aop:pointcut id="myTxPointcut" expression="execution(* com.xpress.service..*(..))"/>
    <!-- 配置切面 -->
    <aop:advisor advice-ref="myTxAdvice" pointcut-ref="myPointcut"/>
</aop:config>

注解方式

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
       default-lazy-init="true" default-autowire="byName">
    <!--配置transactionManager-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="datasource"/>
    </bean>
    <!-- 开启事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
// 添加事务控制注解
@Transactional(rollbackFor = ServiceException.class)
public class UserDao {
    private JdbcTemplate jdbcTemplate;
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    public void updateUser() {
        int n = jdbcTemplate.update("UPDATE users SET username = 'smith' WHERE id=2");
        System.out.println(n);
    }
}
@Transactional
  • rollbackFor = ServiceException.class 指定回滚的异常类型
  • noRollbackFor = NoRollbackException.class 指定不会滚的异常类型
  • propagation = Propagation.REQUIRED 指定事务传播性
  • readOnly = true 设置是否只读事务,只读事务只能读取
  • timeout = 10000 事务超时时间 seconds
  • isolation = Isolation.READ_COMMITTED 数据库的隔离级别

事务控制

  • 默认回滚RuntimeException,checked Exception不会回滚
事务传播属性
  • REQUIRED
    • 业务方法需要在一个事务中运行,如果方法运行时已经存在一个事务中,则加入到该事务,否则自己创建一个事务
  • NOT_SUPPORTED
    • 声明方法不需要事务,如果方法没有关联到一个事务,容器不会为它开启事务,如果方法在一个事务中被调用,该事务会被挂起,方法执行结束后,原先的事务恢复执行
  • REQUIRES_NEW
    • 不管是否存在事务,业务方法总会为自己发起一个新的事务,如果方法已经运行在一个事务中,则原有的事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务恢复执行
  • MANDATORY
    • 业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务,如果业务方法没有在事务环境下调用,容器会抛出异常
  • SUPPORTS
    • 如果业务方法在某个事务范围内被调用,则方法称为该事务的一部分,如果业务方法没有在事务范围内被调用,则方法在没有事务的环境下执行
  • NEVER
    • 业务方法绝对不能在业务范围内执行,如果业务方法在某个事务中执行,容器会抛出异常
  • NESTED
    • 如果一个活动的事务存在,则运行在一个嵌套的事务中,如果没有活动的事务,则按REQUIRED属性执行,它使用了单独的事务,这个事务拥有多个可回滚的保存点,内部事务回滚不会对外部事务造成影响,它只对DataSourceTransactionManager有效

缓存

启用缓存支持

@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public ConcurrentMapCacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <cache:annotation-driven/>
    <bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager"/>
</beans>

其实在本质上,@EnableCaching和<cache:annotation-driven>的工作方式是相同的。它们都会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)。根据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。

配置缓存管理器

Spring 3.1内置了五个缓存管理器实现,如下所示:

  • SimpleCacheManager
  • NoOpCacheManager
  • ConcurrentMapCacheManager
  • CompositeCacheManager
  • EhCacheCacheManager

Spring 3.2引入了另外一个缓存管理器,这个管理器可以用在基于JCache(JSR-107)的缓存提供商之中。除了核心的Spring框架,Spring Data又提供了两个缓存管理器:

  • RedisCacheManager(来自于Spring Data Redis项目)
  • GemfireCacheManager(来自于Spring Data GemFire项目)

EhCache

@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public EhCacheCacheManager cacheManager(CacheManager cacheManager) {
        return new EhCacheCacheManager(cacheManager);
    }
    @Bean
    public EhCacheManagerFactoryBean cacheManagerFactoryBean() {
        EhCacheManagerFactoryBean ehCacheFactoryBean = new EhCacheManagerFactoryBean();
        ehCacheFactoryBean.setConfigLocation(new ClassPathResource("spittr/cache/ehcache.xml"));
        return ehCacheFactoryBean;
    }
}
<ehcache>
  <cache name="spittleCache" maxBytesLocalHeap="50m" timeToLiveSeconds="100"/>
</ehcache>

Redis

@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
    return new RedisCacheManager(redisTemplate);
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
    jedisConnectionFactory.setHostName("localhost");
    jedisConnectionFactory.setPort(6379);
    jedisConnectionFactory.setPassword("pass");
    return jedisConnectionFactory;
}
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, String> redis = new RedisTemplate<>();
    redis.setConnectionFactory(redisConnectionFactory);
    return redis;
}

使用多个缓存管理器

@Bean
public CacheManager cacheManager(net.sf.ehcache.CacheManager cm, javax.cache.CacheManager jm) {
    CompositeCacheManager compositeCacheManager = new CompositeCacheManager();
    List<CacheManager> cacheManagers = new ArrayList<>();
    cacheManagers.add(new JCacheCacheManager(jm));
    cacheManagers.add(new EhCacheCacheManager(cm));
    cacheManagers.add(new RedisCacheManager(redisTemplate(redisConnectionFactory())));
    compositeCacheManager.setCacheManagers(cacheManagers);
    return compositeCacheManager;
}

当查找缓存条目时,CompositeCacheManager首先会从JCacheCacheManager开始检查JCache实现,然后通过EhCacheCacheManager检查Ehcache,最后会使用RedisCacheManager来检查Redis,完成缓存条目的查找。

缓存注解

Spring提供了四个注解来声明缓存规则:

注解 描述
@Cacheable 表明Spring在调用方法之前,首先应该在缓存中查找方法的返回值。如果这个值能够找到,就会返回缓存的值。否则的话,这个方法就会被调用,返回值会放到缓存之中
@CachePut 表明Spring应该将方法的返回值放到缓存中。在方法的调用前并不会检查缓存,方法始终都会被调用
@CacheEvict 表明Spring应该在缓存中清除一个或多个条目
@Caching 这是一个分组的注解,能够同时应用多个其他的缓存注解

填充缓存

  • @Cacheable首先在缓存中查找条目,如果找到了匹配的条目,那么就不会对方法进行调用了。如果没有找到匹配的条目,方法会被调用并且返回值要放到缓存之中。
  • @CachePut并不会在缓存中检查匹配的值,目标方法总是会被调用,并将返回值添加到缓存之中。

@Cacheable和@CachePut有一些共有的属性:

属性 类型 描述
value String[] 要使用的缓存名称
condition String SpEL表达式,如果得到的值是false的话,不会将缓存应用到方法调用上
key String SpEL表达式,用来计算自定义的缓存key
unless String SpEL表达式,如果得到的值是true的话,返回值不会放到缓存之中
public interface SpittleRepository {
    @Cacheable("spittleCache")
    Spittle findOne(long id);
}

当findOne()被调用时,缓存切面会拦截调用并在缓存中查找之前以名spittleCache存储的返回值。缓存的key是传递到findOne()方法中的id参数。

当为接口方法添加注解后,@Cacheable注解会被SpittleRepository的所有实现继承,这些实现类都会应用相同的缓存规则。

计算缓存Key

Spring提供了多个用来定义缓存规则的SpEL扩展:

表达式 描述
#root.args 传递给缓存方法的参数,形式为数组
#root.caches 该方法执行时所对应的缓存,形式为数组
#root.target 目标对象
#root.targetClass 目标对象的类,是#root.target.class的简写形式
#root.method 缓存方法
#root.methodName 缓存方法的名字,是#root.method.name的简写形式
#result 方法调用的返回值(不能用在@Cacheable注解上)
#Argument 任意的方法参数名(如#argName)或参数索引(如#a0或#p0)
@CachePut(value="spittleCache", key="#result.id")
Spittle save(Spittle spittle);

条件化缓存

  • unless属性只能阻止将对象放进缓存,但是在这个方法调用的时候,依然会去缓存中进行查找,如果找到了匹配的值,就会返回找到的值。
  • 与之不同,如果condition的表达式计算结果为false,那么在这个方法调用的过程中,缓存是被禁用的。就是说,不会去缓存进行查找,同时返回值也不会放进缓存中。
@Cacheable(value = "spittleCache", unless = "#result.message.contains('NoCache')")
void getSpittlePageable(int start, int end);

在一定的条件下,我们既不希望将值添加到缓存中,也不希望从缓存中获取数据。

@Cacheable(value = "spittleCache", unless = "#result.message.contains('NoCache')", condition="#id >= 10")
void getSpittlePageable(int start, int end);

unless属性的表达式能够通过#result引用返回值。这是很有用的,这么做之所以可行是因为unless属性只有在缓存方法有返回值时才开始发挥作用。而condition肩负着在方法上禁用缓存的任务,因此它不能等到方法返回时再确定是否该关闭缓存。这意味着它的表达式必须要在进入方法时进行计算,所以我们不能通过#result引用返回值。

移除缓存

@CacheEvict并不会往缓存中添加任何东西。相反,如果带有@CacheEvict注解的方法被调用的话,那么会有一个或更多的条目会在缓存中移除。

与@Cacheable和@CachePut不同,@CacheEvict能够应用在返回值为void的方法上,而@Cacheable和@CachePut需要非void的返回值,它将会作为放在缓存中的条目。因为@CacheEvict只是将条目从缓存中移除,因此它可以放在任意的方法上,甚至void方法。

@CacheEvict(value = "spittleCache")
void delete(long id);

当remove()调用时,会从缓存中删除一个条目。被删除条目的key与传递进来的id参数的值相等。

@CacheEvict注解的属性,指定了哪些缓存条目应该被移除掉:

属性 类型 描述
value String[] 要使用的缓存名称
key String SpEL表达式,用来计算自定义的缓存key
condition String SpEL表达式,如果得到的值是false的话,缓存不会应用到方法调用上
allEntries b oolean 如果为true的话,特定缓存的所有条目都会被移除掉
beforeInvocation b oolean 如果为true的话,在方法调用之前移除条目。如果为false(默认值)的话,在方法成功调用之后再移除条目

XML

Spring的cache命名空间提供了以XML方式配置缓存规则的元素:

元素 描述
<cache:annotation-driven> 启用注解驱动的缓存。等同于Java配置中的@EnableCaching
<cache:advice> 定义缓存通知(advice)。结合<aop:advisor>,将通知应用到切点上
<cache:caching> 在缓存通知中,定义一组特定的缓存规则
<cache:cacheable> 指明某个方法要进行缓存。等同于@Cacheable注解
<cache:cache-put> 指明某个方法要填充缓存,但不会考虑缓存中是否已有匹配的值。等同于@CachePut注解
<cache:cache-evict> 指明某个方法要从缓存中移除一个或多个条目,等同于@CacheEvict注解
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
    <cache:annotation-driven/>
    <bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager"/>
    <aop:config>
        <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* spittr.db.SpittleRepository.*(..))"/>
    </aop:config>

    <cache:advice id="cacheAdvice" cache-manager="cacheManager">
        <cache:caching>
            <cache:cacheable cache="spittleCache" method="findRecent"/>
            <cache:cacheable cache="spittleCache" method="findOne"/>
            <cache:cacheable cache="spittleCache" method="findBySpittleId"/>
            <cache:cache-put cache="spittleCache" method="save" key="#result.id"/>
            <cache:cache-evict cache="spittleCache" method="remove"/>
        </cache:caching>
    </cache:advice>
</beans>

<cache:caching>有几个可以供<cache:cacheable><cache:cache-put><cache:cache-evict>共享的属性,包括:

  • cache:指明要存储和获取值的缓存
  • condition:SpEL表达式,如果计算得到的值为false,将会为这个方法禁用缓存
  • key:SpEL表达式,用来得到缓存的key(默认为方法的参数)
  • method:要缓存的方法名

<cache:cacheable><cache:cache-put>还有一个unless属性,可以为这个可选的属性指定一个SpEL表达式,如果这个表达式的计算结果为true,那么将会阻止将返回值放到缓存之中。

<cache:cache-evict>元素还有几个特有的属性:

  • all-entries:如果是true的话,缓存中所有的条目都会被移除掉。如果是false的话,只有匹配key的条目才会被移除掉。
  • before-invocation:如果是true的话,缓存条目将会在方法调用之前被移除掉。如果是false的话,方法调用之后才会移除缓存。

all-entries和before-invocation的默认值都是false。这意味着在使用<cache:cache-evict>元素且不配置这两个属性时,会在方法调用完成后只删除一个缓存条目。要删除的条目会通过默认的key(基于方法的参数)进行识别,当然也可以通过为名为key的属性设置一个SpEL表达式指定要删除的key。

乱码处理

配置CharacterEncodingFilter过滤器

<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

i18n

<!-- 配置资源文件 -->
<!-- As an alternative to ResourceBundleMessageSource, 
     Spring provides a ReloadableResourceBundleMessageSource class. 
     This variant supports the same bundle file format but is more flexible 
     than the standard JDK based ResourceBundleMessageSource implementation. 
     In particular, it allows for reading files from any Spring resource location (not just from the classpath) 
     and supports hot reloading of bundle property files  -->
<bean id="messageSource"
      class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
        <list>
            <!--xpress.properties
                xpress_en_US.properties
                xpress_zh_CN.properties-->
            <value>xpress</value>
            <!-- ReloadableResourceBundleMessageSource -->
            <!-- <value>classpath:xpress</value> -->
        </list>
    </property>
    <!--Set whether to use the message code as default message instead of throwing a NoSuchMessageException.-->
    <property name="useCodeAsDefaultMessage" value="true" />
</bean>
<!-- MessageSourceAccessor提供了省略Locale的调用方法 -->
<bean id="messageSourceAccessor" class="org.springframework.context.support.MessageSourceAccessor">
    <constructor-arg ref="messageSource" name="messageSource"/>
</bean>
// 默认Locale
messageSource.getMessage("welcome", new Object[]{"admin", new Date()}, Locale.SIMPLIFIED_CHINESE);
new MessageSourceAccessor(messageSource).getMessage("welcome", new Object[]{"admin", new Date()});
// 当前ActionContext Locale
messageSource.getMessage("welcome", new Object[]{"admin", new Date()}, ActionContext.getContext().getLocale());
messageSourceAccessor.getMessage("welcome", new Object[]{"admin", new Date()}, ActionContext.getContext().getLocale());

事件发布及处理

spring event 可以简单实现业务解耦

Event handling in the ApplicationContext is provided through the ApplicationEvent class and ApplicationListener interface.

spring提供的默认事件

  • ContextRefreshedEvent
  • ContextStartedEvent
  • ContextStoppedEvent
  • ContextClosedEvent
  • RequestHandledEvent

event

实现Spring事件发布

开启事务异步处理注解支持

<!-- 开启@AspectJ AOP代理 -->
<aop:aspectj-autoproxy/>
<!-- 任务调度器 -->
<task:scheduler id="scheduler" pool-size="10"/>
<!-- 任务执行器 -->
<task:executor id="executor" pool-size="10"/>
<!--开启注解调度支持 @Async @Scheduled-->
<task:annotation-driven executor="executor" scheduler="scheduler"/>

事件发布器实现接口ApplicationEventPublisherAware

@Service("eventPublisherService")
public class EventPublisherServiceImpl implements EventPublisherService,ApplicationEventPublisherAware {
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {

        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void executePublicEvent(ApplicationEvent event) {
        applicationEventPublisher.publishEvent(event);
    }
}

自定义事件

public class MyEvent extends ApplicationEvent {
    private String eventCode;
    public MyEvent(Object source, String eventCode) {
        super(source);
        this.eventCode = eventCode;
    }
}

定义事件监听器

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    @Async // 注解异步处理
    public void onApplicationEvent(MyEvent event) {
        System.out.println(event.getSource());// do something..
    }
}
//Spring 4.2以后
@EventListener(condition = "#event.shouldSendMsg")
public void afterRegisterSendMail(MessageEvent event) {
     mailService.send(event.getUser().getEmail(),"register successful");
}

异步

全局异步

<!-- 任务执行器 -->
<task:executor id="executor" pool-size="10"/>
<!-- 名字必须是applicationEventMulticaster和messageSource是一样的,默认找这个名字的对象 -->
<!-- 名字必须是applicationEventMulticaster,因为AbstractApplicationContext默认找个 -->
<!-- 如果找不到就new一个,但不是异步调用而是同步调用 -->
<bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
    <!-- 注入任务执行器 这样就实现了异步调用(缺点是全局的,要么全部异步,要么全部同步(删除这个属性即是同步))  -->
    <property name="taskExecutor" ref="executor"/>
</bean>

非全局异步

见实现Spring事件发布部分,开启注解并根据@Async注解实现异步处理,非注解为同步处理

有序

实现SmartApplicationListener接口即可。
或者使用@Order注解

事务控制

  • 同步:同步会延用已经存在的事务控制
    • 在service中发布同步事件,事件处理中调用service,则在一个事务中处理
  • 异步:分别开启不同的连接进行处理
    • 推荐开启第一个事务处理,结束后发布事件,防止在第一个事务中发布事件而事件执行时机不可预知第一个事务数据不能被事务处理读取的问题
    • 监控事务提交后处理事件

监控事务提交后处理事件

@EventListener
public void afterRegisterSendMail(MessageEvent event) {
    // Spring 4.2 之前
    if (TransactionSynchronizationManager.isActualTransactionActive()) {
        TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronizationAdapter() {
                    @Override
                    public void afterCommit() {
                        mailService.send(event);
                    }
                });
    } else {
        mailService.send(event);
    }
}
// Spring 4.2 +
// BEFORE_COMMIT
// AFTER_COMMIT
// AFTER_ROLLBACK
// AFTER_COMPLETION
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterRegisterSendMail(MessageEvent event) {
    mailService.send(event);
}

框架整合

参看整合框架对应篇幅

远程服务

作为一个Java开发者,我们有多种可以使用的远程调用技术,包括:

  • 远程方法调用(Remote Method Invocation,RMI)
  • Caucho的Hessian和Burlap
  • Spring基于HTTP的远程服务
  • 使用JAX-RPC和JAX-WS的Web Service

原理

  • 一部分在服务器端,主要对服务导出进行配置,在这个服务导出中,需要对ServiceExporter类进行配置,比如,定义提供服务的远端服务对象、提供服务的URL地址等;
  • 另一部分在客户端,需要对ProxyFactoryBean进行配置。在Spring远端调用模块中,由ServiceExporter和ProxyFactoryBean来封装远端调用客户端和服务器端的处理,比如,对HTTP调用器、Hessian/Burlap,以及RMI远端调用这些远端调用方案,都可以看到ServiceExporter和ProxyFactory的活跃身影。

Spring远端调用的类设计(客户端封装部分):

remote-support-hierarchy-client

Spring远端调用的类设计(对服务器端的封装)

remote-support-hierarchy-server

HTTP调用器客户端实现的时序:

httpinvoker-afterpropertiesset

HTTP调用器服务器端实现时序:

httpinvoker-handleRequest

Spring RMI客户端实现的设计时序:

RMI-client

Spring远程调用概览

Spring通过多种远程调用技术支持RPC:

RPC模型 适用场景
远程方法调用(RMI) 不考虑网络限制时(例如防火墙),访问/发布基于Java的服务
Hessian或Burlap 考虑网络限制时,通过HTTP访问/发布基于Java的服务。Hessian是二进制协议,而Burlap是基于XML的
HTTP invoker(过时,变为HTTP Support) 考虑网络限制,并希望使用基于XML或专有的序列化机制实现Java序列化时,访问/发布基于Spring的服务
JAX-RPC和JAX-WS 访问/发布平台独立的、基于SOAP的Web服务

在所有的模型中,服务都作为Spring所管理的bean配置到我们的应用中。这是通过一个代理工厂bean实现的,这个bean能够把远程服务像本地对象一样装配到其他bean的属性中去。

remote-proxy

客户端向代理发起调用,就像代理提供了这些服务一样。代理代表客户端与远程服务进行通信,由它负责处理连接的细节并向远程服务发起调用。

更重要的是,如果调用远程服务时发生java.rmi.RemoteException异常,代理会处理此异常并重新抛出非检查型异常RemoteAccessException。远程异常通常预示着系统发生了无法优雅恢复的问题,如网络或配置问题。

RMI

开发和访问RMI服务是非常乏味无聊的,它涉及到好几个步骤,包括程序的和手工的。 Spring简化了RMI模型,它提供了一个代理工厂bean,能让我们把RMI服务像本地JavaBean那样装配到我们的Spring应用中。 Spring还提供了一个远程导出器,用来简化把Spring管理的bean转换为RMI服务的工作。

导出RMI服务

创建RMI服务涉及如下几个步骤:

1. 编写一个服务实现类,类中的方法必须抛出java.rmi.RemoteException异常 2. 创建一个继承于java.rmi.Remote的服务接口 3. 运行RMI编译器(rmic),创建客户端stub类和服务端skeleton类 4. 启动一个RMI注册表,以便持有这些服务 5. 在RMI注册表中注册服务

在Spring中配置RMI服务

RmiServiceExporter可以把任意Spring管理的bean发布为RMI服务。

RmiServiceExporter把bean包装在一个适配器类中,然后适配器类被绑定到RMI注册表中,并且代理到服务类的请求

rmi-service-exporter

@Bean
public RmiServiceExporter rmiExporter(SpittleService spittleService) {
    RmiServiceExporter rmiServiceExporter = new RmiServiceExporter();
    rmiServiceExporter.setService(spittleService);
    rmiServiceExporter.setServiceInterface(SpittleService.class);
    rmiServiceExporter.setServiceName("spittleService");
    rmiServiceExporter.setRegistryHost("rmi.xpress.com");
    rmiServiceExporter.setRegistryPort(1099);
    return rmiServiceExporter;
}

默认情况下,RmiServiceExporter会尝试绑定到本地机器1099端口上的RMI注册表。如果在这个端口没有发现RMI注册表,RmiServiceExporter将会启动一个注册表。如果希望绑定到不同端口或主机上的RMI注册表,那么我们可以通过registryPort和registryHost属性来指定

装配RMI服务

传统上,RMI客户端必须使用RMI API的Naming类从RMI注册表中查找服务

String serviceUrl = "rmi:/spitter/spittleService";
try {
    SpittleService spittleService = (SpittleService) Naming.lookup(serviceUrl);
} catch (NotBoundException e) {
    e.printStackTrace();
} catch (MalformedURLException e) {
    e.printStackTrace();
} catch (RemoteException e) {
    e.printStackTrace();
}

虽然这段代码可以获取Spitter的RMI服务的引用,但是它存在两个问题:

  • 传统的RMI查找可能会导致3种检查型异常的任意一种(RemoteException、NotBoundException和MalformedURLException),这些异常必须被捕获或重新抛出;
  • 需要Spitter服务的任何代码都必须自己负责获取该服务。这属于样板代码,与客户端的功能并没有直接关系。

Spring的RmiProxyFactoryBean是一个工厂bean,该bean可以为RMI服务创建代理。使用RmiProxyFactoryBean引用SpitterService的RMI服务是非常简单的,只需要在客户端的Spring配置中增加如下的@Bean方法:

@Bean
public RmiProxyFactoryBean spittleService() {
    RmiProxyFactoryBean rmiProxyFactoryBean = new RmiProxyFactoryBean();
    rmiProxyFactoryBean.setServiceUrl("rmi:/localhost/spittleService");
    rmiProxyFactoryBean.setServiceInterface(SpittleService.class);
    return rmiProxyFactoryBean;
}

RmiProxyFactoryBean生成一个代理对象,该对象代表客户端来负责与远程的RMI服务进行通信。客户端通过服务的接口与代理进行交互,就如同远程服务就是一个本地的POJO

rmi-proxy-factory-bean

RMI是一种实现远程服务交互的好办法,但是它存在某些限制:

  • 首先,RMI很难穿越防火墙,这是因为RMI使用任意端口来交互——这是防火墙通常所不允许的。在企业内部网络环境中,我们通常不需要担心这个问题。但是如果在互联网上运行,我们用RMI可能会遇到麻烦。即使RMI提供了对HTTP的通道的支持(通常防火墙都允许),但是建立这个通道也不是件容易的事。
  • 另外一件需要考虑的事情是RMI是基于Java的。这意味着客户端和服务端必须都是用Java开发的。因为RMI使用了Java的序列化机制,所以通过网络传输的对象类型必须要保证在调用两端的Java运行时中是完全相同的版本。

Spring的HttpInvoker

需要spring-integration-http包的支持

HTTP invoker是一个新的远程调用模型,作为Spring框架的一部分,能够执行基于HTTP的远程调用(让防火墙不为难),并使用Java的序列化机制(让开发者也乐观其变)。使用基于HTTP invoker的服务和使用基于Hessian/Burlap的服务非常相似。

导出HTTP服务

HttpInvokerServiceExporter工作方式与Hessian和Burlap很相似,通过Spring MVC的DispatcherServlet接收请求,并将这些请求转换成对Spring bean的方法调用

httpinvoker

@Bean
public HandlerMapping httpInvokerMapping() {
    SimpleUrlHandlerMapping simpleUrlHandlerMapping = new SimpleUrlHandlerMapping();
    Properties mappings = new Properties();
    mappings.setProperty("/spitter.service", "httpExportedSpitterService");
    simpleUrlHandlerMapping.setMappings(mappings);
    return simpleUrlHandlerMapping;
}
@Bean
public HttpInvokerServiceExporter httpExportedSpitterService(SpitterService spitterService) {
    HttpInvokerServiceExporter httpInvokerServiceExporter = new HttpInvokerServiceExporter();
    httpInvokerServiceExporter.setService(spitterService);
    httpInvokerServiceExporter.setServiceInterface(SpitterService.class);
    return httpInvokerServiceExporter;
}

访问HTTP服务

HttpInvokerProxyFactoryBean是一个代理工厂bean,用于生成一个代理,该代理使用Spring特有的基于HTTP协议进行远程通信

httpinvokerproxyfactorybean

@Bean
public HttpInvokerProxyFactoryBean spitterService() {
    HttpInvokerProxyFactoryBean httpInvokerProxyFactoryBean = new HttpInvokerProxyFactoryBean();
    httpInvokerProxyFactoryBean.setServiceUrl("http://localhost/spitter/spitter.service");
    httpInvokerProxyFactoryBean.setServiceInterface(SpitterService.class);
    return httpInvokerProxyFactoryBean;
}

HTTP invoker有一个重大的限制:

它只是一个Spring框架所提供的远程调用解决方案。这意味着客户端和服务端必须都是Spring应用。 并且,至少目前而言,也隐含表明客户端和服务端必须是基于Java的。 另外,因为使用了Java的序列化机制,客户端和服务端必须使用相同版本的类(与RMI类似)。

基于Spring的JAX-WS端点

导出独立的JAX-WS端点

SimpleJaxWsServiceExporter不需要为它指定一个被导出bean的引用,它会将使用JAX-WS注解所标注的所有bean发布为JAX-WS服务。

// 配置注解导出的基础配置
@Bean
public SimpleJaxWsServiceExporter jaxWsServiceExporter() {
    SimpleJaxWsServiceExporter simpleJaxWsServiceExporter = new SimpleJaxWsServiceExporter();
    simpleJaxWsServiceExporter.setBaseAddress("http://localhost:8888/services/");// 默认8080
    return simpleJaxWsServiceExporter;
}
// http://localhost:8888/services/spitterService
@Component
@WebService(serviceName = "spitterService")
public class SpitterServiceEndpoint {
    @Autowired
    private SpitterService spitterService;
    @WebMethod
    public void addSpittle(Spittle spittle) {
        spitterService.saveSpittle(spittle);
    }
    @WebMethod
    public void deleteSpittle(Long id) {
        spitterService.deleteSpittle(id);
    }
    @WebMethod
    public void getRecentSpittle(int count) {
        spitterService.getRecentSpittle(count);
    }
    @WebMethod
    public List<Spittle> getRecentSpittle(Spitter spitter) {
        return spitterService.getSpittlesForSpitter(spitter);
    }
}

在客户端代理JAX-WS服务

JaxWsPortProxyFactoryBean生成可以与远程Web服务交互的代理。这些代理可以被装配到其他bean中,就像它们是本地POJO一样

jaxwsportproxyfactorybean

@Bean
public JaxWsPortProxyFactoryBean spitterService() throws MalformedURLException {
    JaxWsPortProxyFactoryBean jaxWsPortProxyFactoryBean = new JaxWsPortProxyFactoryBean();
    jaxWsPortProxyFactoryBean.setWsdlDocumentUrl(new URL("http://localhost:8888/services/spitterService?wsdl"));
    jaxWsPortProxyFactoryBean.setServiceInterface(SpitterService.class);
    jaxWsPortProxyFactoryBean.setPortName("spitterServiceHttpPort");
    jaxWsPortProxyFactoryBean.setServiceName("spitterService");
    jaxWsPortProxyFactoryBean.setNamespaceUri("http://spitter.com");
    return jaxWsPortProxyFactoryBean;
}

wsdlDocumentUrl属性标识了远程Web服务定义文件的位置。JaxWsPortProxyFactory bean将使用这个位置上可用的WSDL来为服务创建代理。由JaxWsPortProxyFactoryBean所生成的代理实现了serviceInterface属性所指定的SpitterService接口。

虽然不太可能这么做,但是在服务的WSDL中定义多个服务和端口是允许的。鉴于此,JaxWsPortProxyFactoryBean需要我们使用portName和serviceName属性指定端口和服务名称。WSDL中<wsdl:port><wsdl:service>元素的name属性可以帮助我们识别出这些属性该设置成什么。

最后,namespaceUri属性指定了服务的命名空间。命名空间将有助于JaxWsPortProxyFactoryBean去定位WSDL中的服务定义。正如端口和服务名一样,我们可以在WSDL中找到该属性的正确值。它通常会在<wsdl:definitions>的targetNamespace属性中。

消息

异步消息简介

在异步消息中有两个主要的概念:消息代理(message broker)和目的地(destination)。

  • 当一个应用发送消息时,会将消息交给一个消息代理。消息代理可以确保消息被投递到指定的目的地,同时解放发送者,使其能够继续进行其他的业务。
  • 尽管不同的消息系统会提供不同的消息路由模式,但是有两种通用的目的地:队列(queue)和主题(topic)。每种类型都与特定的消息模型相关联,分别是点对点模型(队列)和发布/订阅模型(主题)。

点对点消息模型

消息队列对消息发送者和消息接收者进行了解耦。虽然队列可以有多个接收者,但是每一条消息只能被一个接收者取走

p2p

只需要简单地为队列添加新的监听器就能提高应用的消息处理能力。

发布—订阅消息模型

与队列类似,主题可以将消息发送者与消息接收者进行解耦。与队列不同的是,主题消息可以发送给多个主题订阅者

pub-sub

异步消息的优点

虽然同步通信比较容易理解,建立起来也很简单,但是采用同步通信机制访问远程服务的客户端存在几个限制,最主要的是:

  • 同步通信意味着等待。当客户端调用远程服务的方法时,它必须等待远程方法结束后才能继续执行。如果客户端与远程服务频繁通信,或者远程服务响应很慢,就会对客户端应用的性能带来负面影响。
  • 客户端通过服务接口与远程服务相耦合。如果服务的接口发生变化,此服务的所有客户端都需要做相应的改变。
  • 客户端与远程服务的位置耦合。客户端必须配置服务的网络位置,这样它才知道如何与远程服务进行交互。如果网络拓扑进行调整,客户端也需要重新配置新的网络位置。
  • 客户端与服务的可用性相耦合。如果远程服务不可用,客户端实际上也无法正常运行。

异步通信的优点:

  • 无需等待
  • 面向消息和解耦
  • 位置独立
  • 确保投递

使用JMS发送消息

Java消息服务(Java Message Service ,JMS)是一个Java标准,定义了使用消息代理的通用API

ActiveMQ

创建连接工厂

ActiveMQConnectionFactory是ActiveMQ自带的连接工厂,在Spring中可以使用如下方式进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="connectionFactory" class="org.apache.activemq.spring.ActiveMQConnectionFactory" p:brokerURL="tcp://localhost:61616"/>
</beans>

使用ActiveMQ自己的Spring配置命名空间来声明连接工厂(适用于ActiveMQ 4.1之后的所有版本)。首先,我们必须确保在Spring的配置文件中声明了amq命名空间:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:amq="http://activemq.apache.org/schema/core"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://activemq.apache.org/schema/core
       http://activemq.apache.org/schema/core/activemq-core.xsd">
    <amq:connectionFactory id="connectionFactory" brokerURL="tcp://localhost:61616"/>
</beans>

声明ActiveMQ消息目的地

<bean id="queue" class="org.apache.activemq.command.ActiveMQQueue" c:_0="spitter.queue"/>
<bean id="topic" class="org.apache.activemq.command.ActiveMQTopic" c:_0="spitter.topic"/>
<amq:queue id="queue" name="spitter.queue"/>
<amq:queue id="topic" name="spitter.topic"/>

不管是哪种类型,都是借助physicalName属性指定消息通道的名称。

Spring的JMS模板

需要spring-integration-jms包支持

Spring的JmsTemplate会捕获标准的JMSException异常,再以Spring的非检查型异常JmsException子类重新抛出

Spring(org.springframework.jms.* 标准的JMS(javax.jms.*
DestinationResolutionException Spring特有的——当Spring无法解析目的地名称时抛出
IllegalStateException IllegalStateException
InvalidClientIDException InvalidClientIDException
InvalidDestinationException InvalidSelectorException
InvalidSelectorException InvalidSelectorException
JmsSecurityException JmsSecurityException
ListenerExecutionFailedException Spring特有的——当监听器方法执行失败时抛出
MessageConversionException Spring特有的——当消息转换失败时抛出
MessageEOFException MessageEOFException
MessageFormatException MessageFormatException
MessageNotReadableException MessageNotReadableException
MessageNotWriteableException MessageNotWriteableException
ResourceAllocationException ResourceAllocationException
SynchedLocalTransactionFailedException Spring特有的——当同步的本地事务不能完成时抛出
TransactionInprogressException TransactionInprogressException
TransactionRolledBackException TransactionRolledBackException
UncategorizedJmsException Spring特有的——当没有其他异常适用时抛出
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate" c:_0-ref="connectionFactory"/>

发送消息

JmsTemplate代表发送者来负责处理发送消息的复杂过程

jms-send

public class SpittleAlertService implements AlertService {
    @Resource
    JmsTemplate jmsTemplate;
    @Override
    public void sendSpittleAlert(Spittle spittle) {
        jmsTemplate.send("spittle.queue", session -> session.createObjectMessage(spittle));
    }
    @Override
    public void sendSpittleAlert(Spittle spittle) {
        jmsTemplate.send("spittle.queue", new MessageCreator() {
            @Override
            public Message createMessage(Session session) throws JMSException {
                return session.createObjectMessage(spittle);
            }
        });
    }
}

设置默认目的地

<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate" c:_0-ref="connectionFactory" p:defaultDestinationName="spitter.queue"/>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate" c:_0-ref="connectionFactory" p:defaultDestination-ref="queue"/>

消息转换

与send()方法不同,convertAndSend()方法并不需要MessageCreator作为参数。这是因为convertAndSend()会使用内置的消息转换器(message converter)为我们创建消息。

@Override
public void sendSpittleAlert(Spittle spittle) {
    jmsTemplate.convertAndSend(spittle);
}

它使用一个MessageConverter的实现类将对象转换为Message。

Spring为通用的转换任务提供了多个消息转换器(所有的消息转换器都位于org.springframework.jms.support.converter包中)

消息转换器 功能
MappingJacksonMessageConverter 使用Jackson JSON库实现消息与JSON格式之间的相互转换
MappingJackson2MessageConverter 使用Jackson 2 JSON库实现消息与JSON格式之间的相互转换
MarshallingMessageConverter 使用JAXB库实现消息与XML格式之间的相互转换
SimpleMessageConverter 实现String与TextMessage之间的相互转换,字节数组与BytesMessage之间的相互转换,Map与MapMessage之间的相互转换以及Serializable对象与ObjectMessage之间的相互转换

默认情况下,JmsTemplate在convertAndSend()方法中会使用SimpleMessageConverter。但是通过将消息转换器声明为bean并将其注入到JmsTemplate的messageConverter属性中,我们可以重写这种行为。

<bean id="messageConverter" class="org.springframework.jms.support.converter.MappingJackson2MessageConverter"/>
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"
      c:_0-ref="connectionFactory"
      p:defaultDestinationName="spitter.queue"
      p:messageConverter-ref="messageConverter"/>

接收消息

使用JmsTemplate从主题或队列中接收消息的时候,只需要简单地调用receive()方法。JmsTemplate会处理其他的事情

jms-receive

@Override
public Spittle receiveSpittleAlert() throws JMSException {
    ObjectMessage receive = (ObjectMessage) jmsTemplate.receive();
    return (Spittle) receive.getObject();
}

为了遵循Spring规避检查型异常的设计理念,我们不建议本方法抛出JMSException异常,所以我们选择捕获该JMSException异常。在catch代码块中,我们使用Spring中JmsUtils的convertJmsAccessException()方法把检查型异常JMSException转换为非检查型异常JmsException。这其实是在其他场景中由JmsTemplate为我们做的事情。

在receiveSpittleAlert()方法中,我们可以改善的一点就是使用消息转换器。

@Override
public Spittle receiveSpittleAlert() throws JMSException {
    return (Spittle) jmsTemplate.receiveAndConvert();
}

使用JmsTemplate接收消息的最大缺点在于receive()和receiveAndConvert()方法都是同步的。这意味着接收者必须耐心等待消息的到来,因此这些方法会一直被阻塞,直到有可用消息(或者直到超时)。

创建消息驱动的POJO

创建消息监听器

// POJO
@Component
public class SpittleAlertHandler {
    public void handleSpittleAlert(Spittle spittle) {
        System.out.println(spittle.getMessage());
    }
}

配置消息监听器

消息监听器容器监听队列和主题。当消息到达时,消息将转给消息监听器(例如消息驱动的POJO)

litenser-container

<jms:listener-container connection-factory="connectionFactory">
    <jms:listener destination="spitter.queue" ref="spittleAlertHandler" method="handleSpittleAlert"/>
</jms:listener-container>

如果ref属性所标示的bean实现了MessageListener,那就没有必要再指定method属性了,默认就会调用onMessage()方法。

使用基于消息的RPC

导出基于JMS的服务

<bean id="alertServiceExporter" class="org.springframework.jms.remoting.JmsInvokerServiceExporter"
      p:service-ref="spittleAlertService"
      p:serviceInterface="spittr.alerts.AlertService"/>
<jms:listener-container connection-factory="connectionFactory">
    <jms:listener destination="spitter.queue" ref="alertServiceExporter"/>
</jms:listener-container>

使用基于JMS的服务

<bean id="spittleAlertService" class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean"
      p:serviceInterface="spittr.alerts.AlertService"
      p:connectionFactory-ref="connectionFactory"
      p:queueName="spitter.queue"/>

使用AMQP实现消息功能

AMQP具有多项JMS所不具备的优势。

  • 首先,AMQP为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。JMS的API协议能够确保所有的实现都能通过通用的API来使用,但是并不能保证某个JMS实现所发送的消息能够被另外不同的JMS实现所使用。而AMQP的线路层协议规范了消息的格式,消息在生产者和消费者间传送的时候会遵循这个格式。这样AMQP在互相协作方面就要优于JMS——它不仅能跨不同的AMQP实现,还能跨语言和平台。
  • 相比JMS,AMQP另外一个明显的优势在于它具有更加灵活和透明的消息模型。使用JMS的话,只有两种消息模型可供选择:点对点和发布-订阅。这两种模型在AMQP当然都是可以实现的,但AMQP还能够让我们以其他的多种方式来发送消息,这是通过将消息的生产者与存放消息的队列解耦实现的。

AMQP简介

AMQP的生产者并不会直接将消息发布到队列中。AMQP在消息的生产者以及传递信息的队列之间引入了一种间接的机制:Exchange。

exchange

消息的生产者将信息发布到一个Exchange。Exchange会绑定到一个或多个队列上,它负责将信息路由到队列上。信息的消费者会从队列中提取数据并进行处理。

AMQP定义了四种不同类型的Exchange,每一种都有不同的路由算法,这些算法决定了是否要将信息放到队列中。根据Exchange的算法不同,它可能会使用消息的routing key和/或参数,并将其与Exchange和队列之间binding的routing key和参数进行对比。如果对比结果满足相应的算法,那么消息将会路由到队列上。否则的话,将不会路由到队列上。

  • Direct:如果消息的routing key与binding的routing key直接匹配的话,消息将会路由到该队列上;
  • Topic:如果消息的routing key与binding的routing key符合通配符匹配的话,消息将会路由到该队列上;
  • Headers:如果消息参数表中的头信息和值都与bingding参数表中相匹配,消息将会路由到该队列上;
  • Fanout:不管消息的routing key和参数表的头信息/值是什么,消息将会路由到所有队列上。

可以将某个Exchange绑定到另外一个Exchange上,创建路由的内嵌等级结构。

借助这四种类型的Exchange,很容易就能想到我们可以定义任意数量的路由模式,而不再仅限于点对点和发布-订阅的方式

配置Spring支持AMQP消息

配置RabbitMQ

配置RabbitMQ连接工厂最简单的方式就是使用Spring AMQP所提供的rabbit配置命名空间。

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/rabbit"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns:c="http://www.springframework.org/schema/c"
             xsi:schemaLocation="http://www.springframework.org/schema/rabbit
             http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
             http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 默认情况下,连接工厂会假设RabbitMQ服务器监听localhost的5672端口,并且用户名和密码均为guest。 -->
    <connection-factory id="connectionFactory"
            host="${rabbitmq.host}"
            port="${rabbitmq.port}"
            username="${rabbitmq.username}"
            password="${rabbitmq.password}"/>
</beans:beans>

尽管不是必须的,但我还选择在这个配置中将rabbit作为首选的命名空间,将beans作为第二位的命名空间。这是因为在这个配置中,我会更多的声明rabbit而不是bean,这样的话,只会有少量的bean元素使用“beans:”前缀,而rabbit元素就能够避免使用前缀了。

声明队列、Exchange以及binding

声明队列、Exchange和binding的一种方式是使用RabbitMQ Channel接口的各种方法。但是直接使用RabbitMQ的Channel接口非常麻烦。

Spring AMQP的rabbit命名空间包含了多个元素,用来创建队列、Exchange以及将它们结合在一起的binding:

元素 作用
<queue> 创建一个队列
<fanout-exchange> 创建一个fanout类型的Exchange
<header-exchange> 创建一个header类型的Exchange
<topic-exchange> 创建一个topic类型的Exchange
<direct-exchange> 创建一个direct类型的Exchange
<bindings><binding/></bindings> 元素定义一个或多个元素的集合。元素创建Exchange和队列之间的binding

这些配置元素要与<admin>元素一起使用。<admin>元素会创建一个RabbitMQ管理组件(administrative component),它会自动创建(如果它们在RabbitMQ代理中尚未存在的话)上述这些元素所声明的队列、Exchange以及binding。

<admin connection-factory="connectionFactory"/>
<queue id="spittleAlertQueue" name="spittle.alert.queue"/>

对于简单的消息来说,我们只需做这些就足够了。这是因为默认会有一个没有名称的direct Exchange,所有的队列都会绑定到这个Exchange上,并且routing key与队列的名称相同。在这个简单的配置中,我们可以将消息发送到这个没有名称的Exchange上,并将routing key设定为spittle.alert.queue,这样消息就会路由到这个队列中。实际上,我们重新创建了JMS的点对点模型。

路由需要我们声明一个或更多的Exchange,并将其绑定到队列上。例如,如果要将消息路由到多个队列中,而不管routing key是什么,我们可以按照如下的方式配置一个fanout以及多个队列:

<admin connection-factory="connectionFactory"/>
<queue name="spittle.alert.queue.1"/>
<queue name="spittle.alert.queue.2"/>
<queue name="spittle.alert.queue.3"/>
<fanout-exchange name="spittle.alert.exchange">
    <bindings>
        <binding queue="spittle.alert.queue.1"/>
        <binding queue="spittle.alert.queue.2"/>
        <binding queue="spittle.alert.queue.3"/>
    </bindings>
</fanout-exchange>

使用RabbitTemplate发送消息

<!-- 默认配置可使convertAndSend重载方法省略参数传递 -->
<template id="rabbitTemplate"
        connection-factory="connectionFactory"
        exchange="spittle.alert.exchange" 
        routing-key="spittle.alert.queue"/>
private final RabbitTemplate rabbitTemplate;
@Autowired
public AlertServiceImpl(RabbitTemplate rabbitTemplate) {
    this.rabbitTemplate = rabbitTemplate;
}
public void sendSpittleAlert(Spittle spittle) {
    rabbitTemplate.convertAndSend("spittle.alert.exchange","spittle.alert.queue",spittle);
}

接受AMQP消息

使用RabbitTemplate来接收消息

<template id="rabbitTemplate"
        connection-factory="connectionFactory"
        exchange="spittle.alert.exchange"
        routing-key="spittle.alert.queue"/>
Spittle spittle = (Spittle)rabbitTemplate.receiveAndConvert("spittle.alert.queue");

调用receive()和receiveAndConvert()方法都会立即返回,如果队列中没有等待的消息时,将会得到null。这就需要我们来管理轮询(polling)以及必要的线程,实现队列的监控。

定义消息驱动的AMQP POJO

我们并非必须同步轮询并等待消息到达,Spring AMQP还提供了消息驱动POJO的支持

@Component
public class SpittleAlertHandler {
    public void handleSpittleAlert(Spittle spittle) {
        System.out.println(spittle.getMessage());
    }
}
<listener-container connection-factory="connectionFactory">
    <!-- queue-names属性的名称使用了复数形式。在这里我们只设定了一个要监听的队列,但是允许设置多个队列的名称,用逗号分割即可。 -->
    <listener ref="spittleListener"
              method="handleSpittleAlert"
              queue-names="spitter.alert.queue"/>
</listener-container>

另外一种指定要监听队列的方法是引用<queue>元素所声明的队列bean。我们可以通过queues属性来进行设置:

<listener-container connection-factory="connectionFactory">
    <!-- 接受逗号分割的queue ID列表 -->
    <listener ref="spittleListener"
              method="handleSpittleAlert"
              queues="spittleAlertQueue"/>
</listener-container>
<queue id="spittleAlertQueue" name="spitter.alert.queue"/>

WebSocket

Spring 4.0为WebSocket通信提供了支持,包括:

  • 发送和接收消息的低层级API;
  • 发送和接收消息的高级API;
  • 用来发送消息的模板;
  • 支持SockJS,用来解决浏览器端、服务器以及代理不支持WebSocket的问题。

Spring的低层级WebSocket API

按照其最简单的形式,WebSocket只是两个应用之间通信的通道。位于WebSocket一端的应用发送消息,另外一端处理消息。因为它是全双工的,所以每一端都可以发送和处理消息。

websocket

WebSocket通信可以应用于任何类型的应用中,但是WebSocket最常见的应用场景是实现服务器和基于浏览器的应用之间的通信。浏览器中的JavaScript客户端开启一个到服务器的连接,服务器通过这个连接发送更新给浏览器。相比历史上轮询服务端以查找更新的方案,这种技术更加高效和自然。

<!-- 使用websocket命名空间 -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:websocket="http://www.springframework.org/schema/websocket"
       xsi:schemaLocation="
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <websocket:handlers>
        <websocket:mapping handler="marcoHandler" path="/marco"/>
        <websocket:sockjs/>
    </websocket:handlers>
    <bean id="marcoHandler" class="marcopolo.MarcoHandler"/>
</beans>
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{};
    }
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfig.class, WebSocketConfig.class};
    }
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setAsyncSupported(true);
    }
}
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(marcoHandler(), "/marco").withSockJS();
    }
    @Bean
    public MarcoHandler marcoHandler() {
        return new MarcoHandler();
    }
}

实现WebSocketHandler的类

public interface WebSocketHandler {
    void afterConnectionEstablished(WebSocketSession session) throws Exception;
    void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;
    void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
    void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
    boolean supportsPartialMessages();
}

扩展AbstractWebSocketHandler

扩展AbstractWebSocketHandler,这是WebSocketHandler的一个抽象实现。

除了重载WebSocketHandler中所定义的五个方法以外,我们还可以重载AbstractWebSocketHandler中所定义的三个方法:

  • handleBinaryMessage()
  • handlePongMessage()
  • handleTextMessage()
public class MarcoHandler extends AbstractWebSocketHandler {
    private static final Logger logger = LoggerFactory.getLogger(MarcoHandler.class);
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        logger.info("Received message: " + message.getPayload());
        Thread.sleep(2000);
        session.sendMessage(new TextMessage("Polo!"));
    }
}
  • TextWebSocketHandler是AbstractWebSocketHandler的子类,它会拒绝处理二进制消息。它重载了handleBinaryMessage()方法,如果收到二进制消息的时候,将会关闭WebSocket连接。
  • 与之类似,BinaryWebSocketHandler也是AbstractWeb-SocketHandler的子类,它重载了handleTextMessage()方法,如果接收到文本消息的话,将会关闭连接。
  • 尽管你会关心如何处理文本消息或二进制消息,或者二者兼而有之,但是你可能还会对建立和关闭连接感兴趣。我们可以重载afterConnectionEstablished()和afterConnectionClosed()

Web客户端

<script th:inline="javascript">
    var sock = new WebSocket('ws://'+window.location.host+'/websocket/macro');
    sock.onopen = function () {
        console.log('Opening');
        sayMarco();
    }
    sock.onmessage = function (e) {
        console.log('Received message: ', e.data);
        $('#output').append('Received "' + e.data + '"<br/>');
        setTimeout(function () {
            sayMarco()
        }, 2000);
    }
    sock.onclose = function () {
        console.log('Closing');
    }
    function sayMarco() {
        console.log('Sending Marco!');
        $('#output').append('Sending "Marco!"<br/>');
        sock.send("Marco!");
    }
    $('#stop').click(function () {
        sock.close()
    });
</script>

不支持WebSocket的场景

// 开启socketjs支持
registry.addHandler(marcoHandler(), "/marco").withSockJS();
<websocket:handlers>
    <websocket:mapping handler="marcoHandler" path="/marco"/>
    <!-- 开启socketjs支持 -->
    <websocket:sockjs/>
</websocket:handlers>
<script th:src="@{/webjars/sockjs-client/0.3.4/sockjs.min.js}"></script>
<script th:src="@{/webjars/jquery/2.0.3/jquery.min.js}"></script>
var sock = new SockJS([[@{/websocket/marco}]]);

STOMP

我们并非必须要使用原生的WebSocket连接。就像HTTP在TCP套接字之上添加了请求-响应模型层一样,STOMP(Simple Text Oriented Messaging Protocol)在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。

启用STOMP消息功能

@Configuration
// 表明这个配置类不仅配置了WebSocket,还配置了基于代理的STOMP消息
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 将“/marcopolo”注册为STOMP端点
        // 这是一个端点,客户端在订阅或发布消息到目的地路径前,要连接该端点。
        registry.addEndpoint("/marcopolo").withSockJS();
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

当消息到达时,目的地的前缀将会决定消息该如何处理。应用程序的目的地以“/app”作为前缀,而代理的目的地以“/topic”和“/queue”作为前缀。以应用程序为目的地的消息将会直接路由到带有@MessageMapping注解的控制器方法中。而发送到代理上的消息,其中也包括@MessageMapping注解方法的返回值所形成的消息,将会路由到代理上,并最终发送到订阅这些目的地的客户端。

Spring简单的STOMP代理是基于内存的,它模拟了STOMP代理的多项功能:

stomp

尽管它模拟了STOMP消息代理,但是它只支持STOMP命令的子集。因为它是基于内存的,所以它并不适合集群,因为如果集群的话,每个节点也只能管理自己的代理和自己的那部分消息。

启用STOMP代理中继

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    // 根据你所选择的STOMP代理不同,目的地的可选前缀也会有所限制。
    // 如,RabbitMQ只允许目的地的类型为“/temp-queue”、“/exchange”、“/topic”、“/queue”、“/amq/queue”和“/reply-queue”。
    registry.enableStompBrokerRelay("/queue", "/topic")
            .setRelayHost("rabbit.someotherserver")
            .setRelayPort(61613)
            .setClientLogin("guest")
            .setClientPasscode("guest");
    registry.setApplicationDestinationPrefixes("/app","/foo");
}

STOMP代理中继会将STOMP消息的处理委托给一个真正的消息代理:

stomp-real

处理来自客户端的STOMP消息

Spring 4.0引入了@MessageMapping注解,它用于STOMP消息的处理,类似于Spring MVC的@RequestMapping注解。

@Controller
public class MarcoController {
    private static final Logger logger = LoggerFactory.getLogger(MarcoController.class);
    @MessageMapping("/marco")// /app/macro /app是隐含的
    public Shout handleShout(Shout incoming) {
        logger.info("Received message: " + incoming.getMessage());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        Shout outgoing = new Shout();
        outgoing.setMessage("Polo!");
        return outgoing;
    }
}
public class Shout {
    private String message;
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

因为我们现在处理的不是HTTP,所以无法使用Spring的HttpMessageConverter实现将负载转换为Shout对象。Spring 4.0提供了几个消息转换器,作为其消息API的一部分。

消息转换器 描述
ByteArrayMessageConverter 实现MIME类型为“application/octet-stream”的消息与byte[]之间的相互转换
MappingJackson2MessageConverter 实现MIME类型为“application/json”的消息与Java对象之间的相互转换
StringMessageConverter 实现MIME类型为“text/plain”的消息与String之间的相互转换

处理订阅

当收到STOMP订阅消息的时候,带有@SubscribeMapping注解的方法将会触发。

@SubscribeMapping的主要应用场景是实现请求-回应模式。在请求-回应模式中,客户端订阅某一个目的地,然后预期在这个目的地上获得一个一次性的响应。

@SubscribeMapping("macro")
public Shout handleSubscription() {
    Shout outgoing = new Shout();
    outgoing.setMessage("Polo!");
    return outgoing;
}

请求-回应模式与HTTP GET的请求-响应模式并没有太大差别,但是,这里的关键区别在于HTTP GET请求是同步的,而订阅的请求-回应模式则是异步的,这样客户端能够在回应可用时再去处理,而不必等待。

JavaScript客户端

var sock = new SockJS([[@{/marcopolo}]]);
var stomp = Stomp.over(sock);
var playload = JSON.stringify({'message': 'Macro!'});
stomp.connect('guest', 'guest', function (frame) {
    stomp.send("/macro", {}, playload)
});

发送消息到客户端

Spring提供了两种发送数据给客户端的方法:

  • 作为处理消息或处理订阅的附带结果
  • 使用消息模板

在处理消息之后,发送消息

通过为方法添加@SendTo注解,重载目的地

@MessageMapping("/marco")
// 按照这个@SendTo注解,消息将会发布到“/topic/shout”。所有订阅这个主题的应用(如客户端)都会收到这条消息。
@SendTo("/topic/shout")
public Shout handleShout(Shout incoming) {
    logger.info("Received message: " + incoming.getMessage());
    Shout outgoing = new Shout();
    outgoing.setMessage("Polo!");
    return outgoing;
}

@SubscribeMapping注解标注的方式也能发送一条消息,作为订阅的回应。(参看上文)

@SubscribeMapping的区别在于这里的Shout消息将会直接发送给客户端,而不必经过消息代理。如果你为方法添加@SendTo注解的话,那么消息将会发送到指定的目的地,这样会经过代理。

在应用的任意地方发送消息

Spring的SimpMessagingTemplate能够在应用的任何地方发送消息,甚至不必以首先接收一条消息作为前提。

使用SimpMessagingTemplate的最简单方式是将它(或者其接口SimpMessage-SendingOperations)自动装配到所需的对象中。

@Service
public class SpittleFeedServiceImpl implements SpittleFeedService {
    private SimpMessagingTemplate messaging;
    @Autowired
    public SpittleFeedServiceImpl(SimpMessagingTemplate messaging) {
        this.messaging = messaging;
    }
    public void broadcastSpittle(Spittle spittle) {
        messaging.convertAndSend("/topic/spittlefeed", spittle);
    }
}

配置Spring支持STOMP的一个副作用就是在Spring应用上下文中已经包含了SimpMessagingTemplate。因此,我们在这里没有必要再创建新的实例。Spittle-FeedServiceImpl的构造器使用了@Autowired注解,这样当创建SpittleFeedService-Impl的时候,就能注入SimpMessagingTemplate(以SimpMessageSendingOperations的形式)了。

var sock = new SockJS([[@{/spittr}]]);
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function (frame) {
    stomp.subscribe("/topic/spittlefeed", handleSpittle);
});
function handleSpittle(message) {
    console.log('Spittle:', message);
}

为目标用户发送消息

在使用Spring和STOMP消息功能的时候,我们有三种方式利用认证用户:

  • @MessageMapping和@SubscribeMapping标注的方法能够使用Principal来获取认证用户
  • @MessageMapping、@SubscribeMapping和@MessageException方法返回的值能够以消息的形式发送给认证用户
  • SimpMessagingTemplate能够发送消息给特定用户

在控制器中处理用户的消息

@MessageMapping("/spittle")
@SendToUser("/queue/notifications")// 注解由特殊handler处理
public Notification handleSpittle(Principal principal, SpittleForm form) {
    Spittle spittle = new Spittle(principal.getName(), form.getText(), new Date());
    spittleRepo.save(spittle);
    feedService.broadcastSpittle(spittle);
    return new Notification("Saved Spittle for user: " + principal.getName());
}
stomp.subscribe("/user/queue/notifications", handleNotification);

function handleNotification(message) {
    console.log('Notification: ', message);
    $('#output').append("<b>Received: " +
            JSON.parse(message.body).message + "</b><br/>")
}

在内部,以“/user”作为前缀的目的地将会以特殊的方式进行处理。这种消息不会通过AnnotationMethodMessageHandler(像应用消息那样)来处理,也不会通过SimpleBrokerMessageHandler或StompBrokerRelayMessageHandler(像代理消息那样)来处理,以“/user”为前缀的消息将会通过UserDestinationMessageHandler进行处理,它会将消息重路由到某个用户独有的目的地上

user-destination-message-handler

UserDestinationMessageHandler的主要任务是将用户消息重新路由到某个用户独有的目的地上。在处理订阅的时候,它会将目标地址中的“/user”前缀去掉,并基于用户的会话添加一个后缀。例如,对“/user/queue/notifications”的订阅最后可能路由到名为“/queue/notifications-user6hr83v6t”的目的地上。

为指定用户发送消息

private Pattern pattern = Pattern.compile("\\@(\\S+)");
public void broadcastSpittle(Spittle spittle) {
    Matcher matcher = pattern.matcher(spittle.getMessage());
    if (matcher.find()) {
        String username = matcher.group(1);
        messaging.convertAndSendToUser(username, "/queue/notifications",
                new Notification("You just got mentioned!"));
    }
}
public class Notification {
    private String message;
    public Notification(String message) {
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

处理消息异常

@ExceptionHandler方法将有机会处理异常。与之类似,我们也可以在某个控制器方法上添加@MessageException-Handler注解,让它来处理@MessageMapping方法所抛出的异常。

@MessageExceptionHandler({SpittleException.class, SQLException.class})
@SendToUser("/queue/errors")
public Throwable handleException(Throwable t) {
    return t;
}

JMX

应用已经部署并且正在运行,单独使用DI并不能帮助我们改变应用的配置。我们希望深入了解正在运行的应用并要在运行时改变应用的配置,此时,就可以使用Java管理扩展(Java Management Extensions,JMX)了。

使用JMX管理应用的核心组件是托管bean(managed bean,MBean)。所谓的MBean就是暴露特定方法的JavaBean,这些方法定义了管理接口。JMX规范定义了如下4种类型的MBean:

  • 标准MBean:标准MBean的管理接口是通过在固定的接口上执行反射确定的,bean类会实现这个接口
  • 动态MBean:动态MBean的管理接口是在运行时通过调用DynamicMBean接口的方法来确定的。因为管理接口不是通过静态接口定义的,因此可以在运行时改变
  • 开放MBean:开放MBean是一种特殊的动态MBean,其属性和方法只限定于原始类型、原始类型的包装类以及可以分解为原始类型或原始类型包装类的任意类型
  • 模型MBean:模型MBean也是一种特殊的动态MBean,用于充当管理接口与受管资源的中介。模型Bean并不像它们所声明的那样来编写。它们通常通过工厂生成,工厂会使用元信息来组装管理接口

Spring的JMX模块可以让我们将Spring bean导出为模型MBean,这样我们就可以查看应用程序的内部情况并且能够更改配置——甚至在应用的运行期。

将Spring bean导出为MBean

Spring的MBeanExporter是将Spring Bean转变为MBean的关键。MBeanExporter可以把一个或多个Spring bean导出为MBean服务器(MBean server)内的模型 MBean。MBean服务器(有时候也被称为MBean代理)是MBean生存的容器。对MBean的访问,也是通过MBean服务器来实现的。

mbean

在XML配置中,<context:mbean-server>元素可以为我们实现该功能。如果使用Java配置的话,我们需要更直接的方式,也就是配置类型为MBeanServerFactoryBean的bean(这也是在XML中<context:mbean-server>元素所作的事情)。

MBeanServerFactoryBean会创建一个MBean服务器,并将其作为Spring应用上下文中的bean。默认情况下,这个bean的ID是mbeanServer。了解到这一点,我们就可以将它装配到MBeanExporter的server属性中用来指定MBean要暴露到哪个 MBean服务器中。

@Bean
public MBeanExporter mBeanExporter(SpittleController spittleController) {
    MBeanExporter mBeanExporter = new MBeanExporter();
    Map<String, Object> beans = new HashMap<>();
    beans.put("spitter:name=SpittleController", spittleController);
    mBeanExporter.setBeans(beans);
    return mBeanExporter;
}

为了对MBean的属性和操作获得更细粒度的控制,Spring提供了几种选择,包括:

  • 通过名称来声明需要暴露或忽略的bean方法
  • 通过为bean增加接口来选择要暴露的方法
  • 通过注解标注bean来标识托管的属性和操作

通过名称暴露方法

MBean信息装配器(MBean info assembler)是限制哪些方法和属性将在MBean上暴露的关键。其中有一个MBean信息装配器是MethodNameBasedMBean-InfoAssembler。这个装配器指定了需要暴露为MBean操作的方法名称列表。

@Bean
public MBeanExporter mBeanExporter(SpittleController spittleController, MBeanInfoAssembler mBeanInfoAssembler) {
    System.out.println("mbean");
    MBeanExporter mBeanExporter = new MBeanExporter();
    Map<String, Object> beans = new HashMap<>();
    beans.put("spittle:name=SpittleController", spittleController);
    mBeanExporter.setAssembler(mBeanInfoAssembler);
    mBeanExporter.setBeans(beans);
    return mBeanExporter;
}
@Bean
public MBeanInfoAssembler mBeanInfoAssembler() {
    MethodNameBasedMBeanInfoAssembler methodNameBasedMBeanInfoAssembler = new MethodNameBasedMBeanInfoAssembler();
    methodNameBasedMBeanInfoAssembler.setManagedMethods(new String[]{"getPageSize", "setPageSize"});
    return methodNameBasedMBeanInfoAssembler;
}

另一个基于方法名称的装配器是MethodExclusionMBeanInfoAssembler。这个MBean信息装配器是MethodNameBaseMBeanInfoAssembler的反操作。

@Bean
public MBeanInfoAssembler mBeanInfoAssembler() {
    MethodExclusionMBeanInfoAssembler methodExclusionMBeanInfoAssembler = new MethodExclusionMBeanInfoAssembler();
    methodExclusionMBeanInfoAssembler.setIgnoredMethods(new String[]{"getPageSize", "setPageSize"});
    return methodExclusionMBeanInfoAssembler;
}

使用接口定义MBean的操作和属性

Spring的InterfaceBasedMBeanInfoAssembler是另一种MBean信息装配器,可以让我们通过使用接口来选择bean的哪些方法需要暴露为MBean的托管操作

public interface SpittleControllerManagedOperations {
    int getPageSize();
    void setPageSize(int pageSize);
}
@Bean
public MBeanInfoAssembler mBeanInfoAssembler() {
    InterfaceBasedMBeanInfoAssembler interfaceBasedMBeanInfoAssembler = new InterfaceBasedMBeanInfoAssembler();
    interfaceBasedMBeanInfoAssembler.setManagedInterfaces(new Class[]{SpittleControllerManagedOperations.class});
    return interfaceBasedMBeanInfoAssembler;
}

这个接口只是为了标识导出的内容,但我们并不需要在代码中直接实现该接口。不过,SpittleController应该实现这个接口,其实也没有其他的原因,只是在MBean和实现类之间应该有一个一致的协议。

使用注解驱动的MBean

在类级别使用了@ManagedResource注解来标识这个bean应该被导出为MBean。使用@ManagedResource注解标注bean并使用@ManagedOperation或@ManagedAttribute注解标注bean的方法。

<context:mbean-export/>
// 因为是controller,所以在WebConfig中引入
@ImportResource("WEB-INF/spring/applicationContext.xml")
@Bean
public AnnotationMBeanExporter annotationMBeanExporter() {
    AnnotationMBeanExporter annotationMBeanExporter = new AnnotationMBeanExporter();
    annotationMBeanExporter.setRegistrationPolicy(RegistrationPolicy.REPLACING_EXISTING);
    return annotationMBeanExporter;
}
@Controller
@RequestMapping("/spittles")
@ManagedResource("spittle:name=SpittleController")
public class SpittleController implements SpittleControllerManagedOperations {
    private static int PAGE_SIZE = 1;
    @ManagedAttribute
    public int getPageSize() {
        return PAGE_SIZE;
    }
    @ManagedOperation
    public void setPageSize(int pageSize) {
        PAGE_SIZE = pageSize;
    }
}

@ManagedOperation会将方法暴露为MBean的托管操作,但是并不会把spittlesPerPage属性暴露为MBean的托管属性。 这是因为在暴露MBean功能时,使用@ManagedOperation注解标注方法是严格限制方法的,并不会把它作为JavaBean的存取器方法。 因此,使用@ManagedOperation可以用来把bean的方法暴露为MBean托管操作,而使用@ManagedAttribute可以把bean的属性暴露为MBean托管属性。

处理MBean冲突

如果名字冲突,默认情况下,MBeanExporter将抛出InstanceAlreadyExistsException异常,该异常表明MBean服务器中已经存在相同名字的MBean。可以通过MBeanExporter的registrationBehaviorName属性或者<context:mbean-export>的registration属性指定冲突处理机制来改变默认行为。

Spring提供了3种借助registrationBehaviorName属性来处理MBean名字冲突的机制:

  • FAIL_ON_EXISTING:如果已存在相同名字的MBean,则失败(默认行为)
  • IGNORE_EXISTING:忽略冲突,同时也不注册新的MBean
  • REPLACING_EXISTING:用新的MBean覆盖已存在的MBean
mBeanExporter.setRegistrationPolicy(RegistrationPolicy.IGNORE_EXISTING);
annotationMBeanExporter.setRegistrationPolicy(RegistrationPolicy.IGNORE_EXISTING);

远程MBean

暴露远程MBean

使MBean成为远程对象的最简单方式是配置Spring的ConnectorServerFactoryBean,ConnectorServerFactoryBean会创建和启动JSR-160 JMXConnectorServer。

默认情况下,服务器使用JMXMP协议并监听端口9875——因此,它将绑定“service:jmx:jmxmp://localhost:9875”。但是我们导出MBean的可选方案并不局限于JMXMP。根据不同JMX的实现,我们有多种远程访问协议可供选择,包括远程方法调用(Remote Method Invocation,RMI)、SOAP、Hessian/Burlap和IIOP(Internet InterORB Protocol)。为MBean绑定不同的远程访问协议,我们仅需要设置ConnectorServerFactoryBean的serviceUrl属性。

@Bean
public ConnectorServerFactoryBean connectorServerFactoryBean() {
    ConnectorServerFactoryBean connectorServerFactoryBean = new ConnectorServerFactoryBean();
    // connectorServerFactoryBean.setServiceUrl("service:jmx:jmxmp://localhost:9875");
    connectorServerFactoryBean.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
    return connectorServerFactoryBean;
}

访问远程MBean

MbeanServerConnectionFactoryBean,该bean用于访问基于RMI的远程服务器。

@Bean
public MBeanServerConnectionFactoryBean mBeanServerConnectionFactoryBean() throws MalformedURLException {
    MBeanServerConnectionFactoryBean mBeanServerConnectionFactoryBean = new MBeanServerConnectionFactoryBean();
    mBeanServerConnectionFactoryBean.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
    return mBeanServerConnectionFactoryBean;
}
// 查询数量
Integer mBeanCount = mBeanServerConnection.getMBeanCount();
// 查询所有MBean名称
Set<ObjectName> objectNames = mBeanServerConnection.queryNames(null, null);
// 调用
Object pageSize = mBeanServerConnection.getAttribute(new ObjectName("spitter:name=SpittleController"), "pageSize");
mBeanServerConnection.setAttribute(new ObjectName("spitter:name=SpittleController"), new Attribute("pageSize", 100));
mBeanServerConnection.invoke(new ObjectName("spitter:name=SpittleController"), "pageSize", new Object[]{100}, new String[]{"int"});

代理MBean:

MBeanFactoryBean创建远程MBean的代理。客户端通过此代理与远程MBean进行交互,就像它是本地Bean一样

@Bean
public MBeanProxyFactoryBean mBeanProxyFactoryBean(MBeanServerConnection mBeanServerConnection) throws MalformedURLException, MalformedObjectNameException {
    MBeanProxyFactoryBean mBeanProxyFactoryBean = new MBeanProxyFactoryBean();
    // mBeanProxyFactoryBean.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
    mBeanProxyFactoryBean.setObjectName("spittle:name=SpittleController");
    mBeanProxyFactoryBean.setServer(mBeanServerConnection);
    mBeanProxyFactoryBean.setProxyInterface(SpittleControllerManagedOperations.class);
    return mBeanProxyFactoryBean;
}
@Autowired
private SpittleControllerManagedOperations spittleControllerManagedOperations;

处理通知

JMX通知(JMX notification)是MBean与外部世界主动通信的一种方法,而不是等待外部应用对MBean进行查询以获得信息。

JMX通知使MBean与外部世界进行主动通信:

jmx-notification

发送通知

实现NotificationPublisherAware接口。这并不是一个要求苛刻的接口,它仅要求实现一个方法:setNotificationPublisher。

@Component
@ManagedResource("spittle:name=SpittleNotifier")
@ManagedNotification(notificationTypes = "SpittleNotifier.notify", name = "TODO")
public class SpittleNotifierImpl implements SpittleNotifier, NotificationPublisherAware {
    private NotificationPublisher notificationPublisher;
    @Override
    public void millionthSpittlePosted() {
        notificationPublisher.sendNotification(new Notification("a notification", this, 0));
    }
    @Override
    public void setNotificationPublisher(NotificationPublisher notificationPublisher) {
        this.notificationPublisher = notificationPublisher;
    }
}

监听通知

@Bean
public MBeanExporter mBeanExporter() {
    MBeanExporter mBeanExporter = new MBeanExporter();
    Map<String, NotificationListener> beans = new HashMap<>();
    beans.put("spittle:name=SpittleNotifier", new SpittleNotificationListener());
    mBeanExporter.setNotificationListenerMappings(beans);
    return mBeanExporter;
}
public class SpittleNotificationListener implements NotificationListener {
    @Override
    public void handleNotification(Notification notification, Object handback) {
    }
}

Spring Boot

它提供了四个主要的特性,能够改变开发Spring应用程序的方式:

  • Spring Boot Starter:它将常用的依赖分组进行了整合,将其合并到一个依赖中,这样就可以一次性添加到项目的Maven或Gradle构建中
  • 自动配置:Spring Boot的自动配置特性利用了Spring 4对条件化配置的支持,合理地推测应用所需的bean并自动化配置它们
  • 命令行接口(Command-line interface,CLI):Spring Boot的CLI发挥了Groovy编程语言的优势,并结合自动配置进一步简化Spring应用的开发
  • Actuator:它为Spring Boot应用添加了一定的管理特性

添加Starter依赖

Spring Boot Starter依赖将所需的常见依赖按组聚集在一起,形成单条依赖:

Starter 所提供的依赖
spring-boot-starter-actuator spring-boot-starter 、spring-boot-actuator 、spring-core
spring-boot-starter-amqp spring-boot-starter 、spring-boot-rabbit 、spring-core 、 spring-tx
spring-boot-starter-aop spring-boot-starter 、spring-aop 、AspectJ Runtime 、AspectJ Weaver 、spring-core
spring-boot-starter-batch spring-boot-starter 、HSQLDB 、spring-jdbc 、spring-batch-core 、spring-core
spring-boot-starter-elasticsearch spring-boot-starter、 spring-data-elasticsearch、 spring-core、 spring-tx
spring-boot-starter-gemfire spring-boot-starter、 Gemfire、 spring-core、 spring-tx、 spring-context、 spring-context-support、 spring-data-gemfire
spring-boot-starter-data-jpa spring-boot-starter、 spring-boot-starter-jdbc、 spring-boot-starter-aop、 spring-core、 Hibernate EntityManager、 spring-orm、 spring-data-jpa、 spring-aspects
spring-boot-starter-data-mongodb spring-boot-starter、 MongoDB Java 驱动 、 spring-core、 spring-tx、 spring-data-mongodb
spring-boot-starter-data-rest spring-boot-starter、 spring-boot-starter-web、 Jackson 注解 、 Jackson 数据绑定 、 spring-core、 spring-tx、 spring-data-rest-webmvc
spring-boot-starter-data-solr spring-boot-starter、 Solrj、 spring-core、 spring-tx、 spring-data-solr、 Apache HTTP Mime
spring-boot-starter-freemarker spring-boot-starter、 spring-boot-starter-web、 Freemarker、 spring-core、 spring-context-support
spring-boot-starter-groovy-templ-ates spring-boot-starter、 spring-boot-starter-web、 Groovy、 Groovy 模板、spring-core
spring-boot-starter-hornetq spring-boot-starter、 spring-core、 spring-jms、 Hornet JMS Client
spring-boot-starter-integration spring-boot-starter、 spring-aop、 spring-tx、 spring-web、 spring-webmvc、 spring-integration-core、 spring-integration-file、 spring-integration-http、 spring-integration-ip、 spring-integration-stream
spring-boot-starter-jdbc spring-boot-starter、 spring-jdbc 、tomcat-jdbc、 spring-tx
spring-boot-starter-jetty jetty-webapp、 jetty-jsp
spring-boot-starter-log4j jcl-over-slf4j、 jul-to-slf4j 、slf4j-log4j12、log4j
spring-boot-starter -logging jcl-over-slf4j、 jul-to-slf4j 、log4j-over-slf4j、 logback-classic
spring-boot-starter-mobile spring-boot-starter、 spring-boot-starter-web、 spring-mobile-device
spring-boot-starter-redis spring-boot-starter、 spring-data-redis、 lettuce
spring-boot-starter-remote-shell spring-boot-starter-actuator、 spring-context、 org.crashub.**
spring-boot-starter-security spring-boot-starter、 spring-security-config、 spring-security-web、 spring-aop、 spring-beans、 spring-context、 spring-core、 spring-expression、 spring-web
spring-boot-starter-social-facebook spring-boot-starter、 spring-boot-starter-web、 spring-core、 spring-social-config、 spring-social-core、 spring-social-web、 spring-social-facebook
spring-boot-starter-social-twitter spring-boot-starter、 spring-boot-starter-web、 spring-core、 spring-social-config、 spring-social-core、 spring-social-web、 spring-social-twitter
spring-boot-starter-social-linkedin spring-boot-starter、 spring-boot-starter-web、 spring-core、 spring-social-config、 spring-social-core、 spring-social-web、 spring-social-linkedin
spring-boot-starter spring-boot、 spring-boot-autoconfigure、 spring-boot-starter-logging
spring-boot-starter-test spring-boot-starter-logging、 spring-boot、 junit、mockito-core、 hamcrest-library、 spring-test
spring-boot-starter-thymeleaf spring-boot-starter、 spring-boot-starter-web、 spring-core、 thymeleaf-spring4、 thymeleaf-layout-dialect
spring-boot-starter-tomcat tomcat-embed-core、 tomcat-embed-logging-juli
spring-boot-starter-web spring-boot-starter、 spring-boot-starter-tomcat、 jackson-databind、 spring-web、 spring-webmvc
spring-boot-starter-websocket spring-boot-starter-web、 spring-websocket、 tomcat-embed-core、 tomcat-embed-logging-juli
spring-boot-starter-ws spring-boot-starter、 spring-boot-starter-web、 spring-core、 spring-jms、 spring-oxm、 spring-ws-core、 spring-ws-support

需要注意,很多Starter引用了其他的Starter。

自动配置

Spring Boot的Starter减少了构建中依赖列表的长度,而Spring Boot的自动配置功能则削减了Spring配置的数量。它在实现时,会考虑应用中的其他因素并推断你所需要的Spring配置。

自动配置的模板解析器会在指定的目录下查找Thymeleaf模板,这个目录也就是相对于根类路径下的templates目录下

在Spring Boot中,有必要讨论一下它是如何处理静态内容的。当采用Spring Boot的Web自动配置来定义Spring MVC bean时,这些bean中会包含一个资源处理器(resource handler),它会将“/**”映射到几个资源路径中。这些资源路径包括(相对于类路径的根):

  • /META-INF/resources/
  • /resources/
  • /static/
  • /public/

在传统的基于Maven/Gradle构建的项目中,我们通常会将静态内容放在“src/main/webapp”目录下,这样在构建所生成的WAR文件里面,这些内容就会位于WAR文件的根目录下。如果使用Spring Boot构建WAR文件的话,这依然是可选的方案。

Spring Boot CLI

Spring Boot CLI充分利用了Spring Boot Starter和自动配置的魔力,并添加了一些Groovy的功能。它简化了Spring的开发流程,通过CLI,我们能够运行一个或多个Groovy脚本,并查看它是如何运行的。

Actuator

Spring Boot Actuator为Spring Boot项目带来了很多有用的特性,包括:

  • 管理端点
  • 合理的异常处理以及默认的“/error”映射端点
  • 获取应用信息的“/info”端点
  • 当启用Spring Security时,会有一个审计事件框架

以上概念总结于传智播客Spring课程,Spring In Action,Spring技术内幕

Search

    Post Directory