Spring 模块
Spring是一个轻量级控制反转(IoC)和面向切面(AOP)的容器开源框架。
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 ,可以将遍布系统的关注点(例如事务和安全)从它们所应用的对象中解耦出来。
数据访问与集成
使用 JDBC 编写代码通常会导致大量的样板式代码,例如获得数据库连接、创建语句、处理结果集到最后关闭数据库连接。 Spring 的 JDBC 和 DAO ( Data Access Object )模块抽象了这些样板式代码,使我们的数据库代码变得简单明了,还可以避免因为关闭数据库资源失败而引发的问题。 对许多流行的 ORM 框架进行了集成,包括 Hibernate 、 Java Persisternce API 、 Java Data Object 和 iBATIS SQL Maps 。 Spring 的事务管理支持所有的ORM框架以及 JDBC 。
Web 与远程调用 MVC ( Model-View-Controller )
MVC模式是一种普遍被接受的构建Web应用的方法,它可以帮助用户将界面逻辑与应用逻辑分离。 Java 从来不缺少 MVC 框架, Apache 的 Struts 、 JSF 、 WebWork 和 Tapestry 都是可选的最流行的 MVC 框架。 虽然 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 )的功能。具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。
测试
鉴于开发者自测的重要性, Spring 提供了测试模块以致力于 Spring 应用的测试。 通过该模块,你会发现 Spring 为使用 JNDI 、 Servlet 和 Portlet 编写单元测试提供了一系列的 mock 对象实现。对于集成测试,该模块为加载 Spring 应用上下文中的 bean 集合以及与 Spring 上下文中的 bean 进行交互提供了支持。
Spring IOC容器
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
IoC 容器的职责主要有两个:
- 业务对象的构建管理。在IoC场景中,业务对象无需关心所依赖的对象如何构建如何取得,但 这部分工作始终需要有人来做。IoC负责将对象的构建逻辑从客户端对象那里剥离出来,以免这部分逻辑污染业务对象的实现。
- 业务对象间的依赖绑定。IoC通过结合之前构建和管理的所有业务对象,以及各个业务对象间可以识别的依赖关系,将这些对象所依赖的对象注入绑定,从而保证每个业务对象在使用的时候,可以处于就绪状态。
Spring提供了两种容器类型:
- BeanFactory。基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景, BeanFactory 是比较合适的 IoC容器选择。
- ApplicationContext 。 ApplicationContext 在 BeanFactory 的基础上构建,是相对比较高级的容器实现,除了拥有 BeanFactory 的所有支持, ApplicationContext 还提供了其他高级 ,比如事件发布、国际化信息支持等。 ApplicationContext 所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。相对于 BeanFactory来说, ApplicationContext 要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory 也会长一些。在那些系统资源充足,并且要求更多功能的场景中, ApplicationContext 类型的容器是比较合适的选择。
ApplicationContext 间接继承自 BeanFactory ,所以说它是构建于 BeanFactory 之上 的IoC容器
Spring 自带了多种类型的应用上下文
- AnnotationConfigApplicationContext :从一个或多个基于 Java 的配置类中加载 Spring 应用上下文。
- AnnotationConfigWebApplicationContext :从一个或多个基于 Java 的配置类中加载 Spring Web 应用上下文。
- ClassPathXmlApplicationContext :从类路径下的一个或多个 XML 配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
- FileSystemXmlapplicationcontext :从文件系统下的一个或多个 XML 配置文件中加载上下文定义。
- XmlWebApplicationContext :从 Web 应用下的一个或多个 XML 配置文件中加载上下文定义。
IoC容器运行阶段
Spring的IoC容器实现以上功能的过程,基本上可以按照类似的流程划分为两个阶段,即容器启动阶段和Bean实例化阶段
1. 容器启动阶段
容器启动开始,首先会通过某种途径加载Configuration MetaData。除了代码方式比较直接,在大部分情况下,容器需要依赖某些工具类( BeanDefinitionReader )对加载的Configuration MetaData进行解析和分析,并将分析后的信息编组为相应的 BeanDefinition ,最后把这些保存了bean定义必要信息的 BeanDefinition ,注册到相应的 BeanDefinitionRegistry ,这样容器启动工作就完成了。
2. Bean实例化阶段
所有的bean定义信息都通过 BeanDefinition 的方式注册到了BeanDefinitionRegistry中。当某个请求方通过容器的 getBean 方法明确地请求某个对象,或者因依赖关系容器需要隐式地调用 getBean 方法时,就会触发第二阶段的活动。 该阶段,容器会首先检查所请求的对象之前是否已经初始化。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。当该对象装配完毕之后,容器会立即将其返回请求方使用。
bean 的生命周期
在基于 Spring 的应用中,你的应用对象生存于 Spring 容器( container )中。 Spring 容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡(在这里,可能就是 new 到 finalize() )
在传统的 Java 应用中, bean 的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java 自动进行垃圾回收。
bean 装载到 Spring 应用上下文中的一个典型的生命周期过程
bean 准备就绪之前, bean 工厂执行了若干启动步骤。
1 . Spring 对 bean 进行实例化;
2 . Spring 将值和 bean 的引用注入到 bean 对应的属性中;
3 .如果 bean 实现了 BeanNameAware 接口, Spring 将 bean 的 ID 传递给 setBean-Name() 方法;
4 .如果 bean 实现了 BeanFactoryAware 接口, Spring 将调用 setBeanFactory() 方法,将 BeanFactory 容器实例传入;
5 .如果 bean 实现了 ApplicationContextAware 接口, Spring 将调用 setApplicationContext() 方法,将 bean 所在的应用上下文的 引用传入进来;
6 .如果 bean 实现了 BeanPostProcessor 接口, Spring 将调用它们的 postProcessBeforeInitialization() 方法;
7 .如果 bean 实现了 InitializingBean 接口, Spring 将调用它们的 afterPropertiesSet() 方法。类似地,如果 bean 使用 init- method 声明了初始化方法,该方法也会被调用;
8 .如果 bean 实现了 BeanPostProcessor 接口, Spring 将调用它们的 post-ProcessAfterInitialization() 方法;
9 .此时, bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
10 .如果 bean 实现了 DisposableBean 接口, Spring 将调用它的 destroy() 接口方法。同样,如果 bean 使用 destroy-method 声明了销 毁方法,该方法也会被调用。
bean 的作用域
在默认情况下, Spring 应用上下文中所有 bean 都是作为以单例( singleton )的形式创建的。也就是说,不管给定的一个 bean 被注入到其他 bean 多少次,每次所注入的都是同一个实例。
Spring 定义了多种作用域,用 @Scope 注解指定,可以基于这些作用域创建 bean ,包括:
- 单例( Singleton ):在整个应用中,只创建 bean 的一个实例。
- 原型( Prototype ):每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新的 bean 实例。
- 会话( Session ):在 Web 应用中,为每个会话创建一个 bean 实例。
- 请求( Rquest ):在 Web 应用中,为每个请求创建一个 bean 实例
@Scope 同时还有一个 proxyMode 属性。这个属性解决了将会话或请求作用域的 bean 注入到单例 bean 中所遇到的问题。
面向切面编程AOP
在使用面向切面编程时,我们仍然在一个地方定义通用功能, 但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称 为切面( aspect )。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁。
AOP 术语
通知( Advice )
切面的工作被称为通知。Spring 切面可以应用 5 种类型的通知:
- 前置通知( Before ):在目标方法被调用之前调用通知功能;
- 后置通知( After ):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知( After-returning ):在目标方法成功执行之后调用通知;
- 异常通知( After-throwing ):在目标方法抛出异常后调用通知;
- 环绕通知( Around ):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
连接点( Join point )
连接点是在应用执行过程中能够插入切面的一个点。这个点可 以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点( Poincut )
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用 明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面( Aspect )
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容 —— 它是什么,在何时和何处完成其功能。
引入( Introduction )
引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个 Auditable 通知类,该类记录了对象最后一次修改时的状态。这很简 单,只需一个方法, setLastModified(Date) ,和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类 中,从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。
织入( Weaving )
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。 AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器( ClassLoader ),它可以在目标类被引入应用之前增强该目标类的字节码。 AspectJ 5 的加载时织入( load-time weaving , LTW )就支持以这种方式织入切面。 运
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时, AOP 容器会为目标对象动态地创建一个代理对象。 Spring AOP 就是以这种方式织入切面的。
Spring 对 AOP 的支持
创建切点来定义切面所织入的连接点是 AOP 框架的基本功能。
Spring 提供了 4 种类型的 AOP 支持
- 基于代理的经典 Spring AOP ;
- 纯 POJO 切面;
- @AspectJ 注解驱动的切面;
- 注入式 AspectJ 切面(适用于 Spring 各版本)。
前三种都是 Spring AOP 实现的变体, Spring AOP 构建在动态代理基础之上,因此, Spring 对 AOP 的支持局限于方法拦截
通过在代理类中包裹切面, Spring 在运行期把切面织入到 Spring 管理的 bean 中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean 。当代理拦截到方法调用时,在调用目标 bean 方法之前,会执行切面逻辑。
Spring AOP的实现机制
Spring AOP采用动态代理机制和字节码生成技术实现。与最初的AspectJ采用编译器将横切逻辑织入目标对象不同,动态代理机制和字节码生成都是在运行期间为目标对象生成一个代理对象,而将横切逻辑织入到这个代理对象中,系统最终使用的是织入了横切逻辑的代理对象,而不是真正的目标对象。
Spring AOP在无法采用动态代理机制进行AOP功能扩展的时候,会使用CGLIB库的动态字节码增强支持来实现AOP的功能扩展。
因为Spring 基于动态代理,所以 Spring 只支持方法连接点。这与一些其他的AOP框架是不同的,例如 AspectJ 和 JBoss ,除了方法切点,它们还提供了字段和构造器接入点。 Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在 bean 创建时应用通知。 但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用 Aspect 来补充 Spring AOP 的功能。
动态代理
JDK1.3之后引入了一种称之为动态代理(Dynamic Proxy)的机制。使用该机制,我们可以为指定的接口在系统运行期间动态地生成代理对象。动态代理机制的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy类和java. lang.reflect.InvocationHandler接口。
当Proxy动态生成的代理对象上相应的接口方法被调用时,对应的InvocationHandler就会拦截相应的方法调用,并进行相应处理。 InvocationHandler就是我们实现横切逻辑的地方,它是横切逻辑的载体,作用跟Advice是一样的。所以,在使用动态代理机制实现AOP的过程中,我们可以在InvocationHandler的基础上细化程序结构,并根据Advice的类型,分化出对应不同Advice类型的程序结构。 动态代理机制只能对实现了相应Interface的类使用,如果某个类没有实现任何的Interface,就无法使用动态代理机制为其生成相应的动态代理对象。而如果目标对象没有实现任何Interface,Spring AOP会尝试使用一个称为CGLIB(CodeGeneration Library)的开源的动态字节码生成类库,为目标对象生成动态的代理对象实例。
动态字节码生成
使用动态字节码生成技术扩展对象行为的原理是,我们可以对目标对象进行继承扩展,为其生成相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系统使用扩展后的目标对象的子类,就可以达到与代理模式相同的效果了。
使用动态字节码增强技术,即使模块类没有实现相应的接口,我们依然可以对其进行扩展,而不用像动态代理那样受限于接口。如果需要扩展的类以及类中的实例方法等声明为final的话,则无法对其进行子类化的扩展。
AOP案例
异常处理
当系统中多个地方都可能抛出异常的时候我们可能会在每个调用的最顶层,分别添加异常处理逻辑对其进行处理,通常就是记录日志。这些相同的逻辑实现可以归并于一处进行处理,它实际上就是一种横切关注点(cross-cuting concern)。
缓存
AOP应用的另一个主要场景是为系统透明地添加缓存支持。 为了避免需要添加的缓存实现逻辑影响业务逻辑的实现,我们可以让缓存的实现独立于业务对象的实现之外,将系统中的缓存需求通过AOP的Aspect进行封装,只在系统中某个点确切需要缓存支持的情况下,才为其织入。
Spring事务管理
事务就是以可控的方式对数据资源进行访问的一组操作。为了保证事务执行前后,数据资源所承载的系统状态始终处于“正确”状态,事务本身持有4个限定属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),这也就是常说的事务的ACID属性。
Spring 事务管理是通过Spring AOP去实现的。Spring的事务框架设计理念的基本原则是:让事务管理的关注点与数据访问关注点相分离。
- 当在业务层使用事务的抽象API进行事务界定的时候,不需要关心事务将要加诸于上的事务资源是什么,对不同的事务资源的管理将由相应的框架实现类来操心。
- 当在数据访问层对可能参与事务的数据资源进行访问的时候,只需要使用相应的数据访问API进行数据访问,不需要关心当前的事务资源如何参与事务或者是否需要参与事务。这同样将由事务框架类来打理。
PlatformTransactionManager是Spring事务抽象架构的核心接口,它的主要作用是为应用程序提供事务界定的统一方式。
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;
//事务提交
void commit(TransactionStatus var1) throws TransactionException;
//事务回滚
void rollback(TransactionStatus var1) throws TransactionException;}
通常都是将事务管理放在Service层,而将数据访问逻辑放在DAO层。这样做的目的是,可以不用因为将事务管理代码放在DAO层,而降低数据访问逻辑的重用性,也可以在Service层根据相应逻辑,来决定提交或者回滚事务。一般的Service对象可能需要在同一个业务方法中调用多个数据访问对象的方法。
事务隔离级别
数据库和程序一样,也有并发的问题,在同时存在两个或者两个以上的数据库事务环境中,同一条记录甚至是不同记录都会由于SQL在不同时刻的执行产生不同的结果,甚至产生错误。于是便有了隔离级别这样的数据库的概念,按照数据库的概念分为脏读、读写提交、可重复读、序列化4种。
TransactionDefinition内定义了如下5个常量用于标志可供选择的隔离级别。
- ISOLATION_DEFAULT。如果指定隔离级别为ISOLATION_DEFAULT,则表示使用数据库默认的隔离级别。
- ISOLATION_READ_UNCOMMITTED。对应Read Uncommited隔离级别,无法避免脏读,不可重复读和幻读。
- ISOLATION_READ_COMMITTED。对应Read Commited隔离级别,可以避免脏读,但无法避免不可重复读和幻读。
- ISOLATION_REPEATABLE_READ。对应Repeatable read隔离级别,可以避免脏读和不可重复读,但不能避免幻读。
- ISOLATTON_SERIALTZABLE。对应Serializable隔离级别,可以避免所有的脏读,不可重复读以及幻读,但并发性效率最低。
不是所有的数据库支持所有的隔离级别,比如Oracle数据库只支持读写提交和序列化,它的默认隔离级别为读写提交,而MySQL数据库的默认隔离级别为可重复读。
事务的传播行为
传播行为,是指方法之间的调用问题。在大部分的情况下,我们认为事务都应该是一次性全部成功或者全部失败的。
针对事务的传播行为,TransactionDefinition提供了以下几种选择
事务的实现
编程式事务管理
通过Spring进行编程式事务管理有两种方式,要么直接使用PlatformrransactionManager,要么使用更方便的Transactionremplate。
TransactionTemplate是Spring提供的进行编程式事务管理的模板方法类,继承了DefaultTransactionDefinition。可以直接通过TransactionTemplate本身提供事务控制属性。
声明式事务管理
注解元数据驱动的声明式事务管理的基本原理是,将对应业务方法的事务元数据,直接通过注解标注到业务方法或者业务方法所在的对象上,然后在业务方法执行期间,通过反射读取标注在该业务方法上的注解所包含的元数据信息,最终将根据读取的信息为业务方法构建事务管理的支持。
Spring MVC
Spring MVC 的请求流程
1. 请求 Spring 的 DispatcherServlet 。与大多数基于 Java 的 Web 框架一样, Spring MVC 所有的请求都会通过一个前端控制器 ( front controller ) Servlet 。前端控制器是常用的 Web 应用程序模式,在这里一个单实例的 Servlet 将请求委托给应用程序的其他组件来执行实际 的处理。在 Spring MVC 中, DispatcherServlet 就是前端控制器。 DispatcherServlet 的任务是将请求发送给 Spring MVC 控制器( controller )。控制器是一个用于处理请求的 Spring 组件。在典型的应用程 序中可能会有多个控制器, DispatcherServlet 需要知道应该将请求发送给哪个控制器。所以 DispatcherServlet 以会查询一个或多个处理器映射( handler mapping ) 来确定请求的下一站在哪里。
2.处理器映射会根据请求所携带的 URL 信息来进行决策。 一旦选择了合适的控制器, DispatcherServlet 会将请求发送给选中的控制器 。
3.到了控制器,请求会卸下其负载(用户提交的信息)并 耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进 行处理。) 控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型( model )。
4.不过仅仅给 用户返回原始的信息是不够的 —— 这些信息需要以用户友好的方式进行格式化,一般会是 HTML 。所以,信息需要发送给一个视图( view ), 通常会是 JSP 。 控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送 回 DispatcherServlet 。 这样,控制器就不会与特定的视图相耦合,传递给 DispatcherServlet 的视图名并不直接表示某个特定的 JSP 。实际上,它甚至并不能确 这样,控制器就不会与特定的视图相耦合,传递给 DispatcherServlet 的视图名并不直接表示某个特定的 JSP 。实际上,它甚至并不能确 定视图就是 JSP 。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。
5.DispatcherServlet 将会使用视图解析器( view resolver ) 来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是 JSP 。 既然 DispatcherServlet 已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。
6.它的最后一站是视图的实现(可能是 JSP ) , 在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端 。
处理异常
Spring 提供了多种方式将异常转换为响应: 特定的 Spring 异常将会自动映射为指定的HTTP 状态码; 异常上可以添加 @ResponseStatus 注解,从而将其映射为某一个 HTTP 状态码; 在方法上可以添加 @ExceptionHandler 注解,使其用来处理异常。 处理异常的最简单方式就是将其映射到 HTTP 状态码上,进而放到响应之中。接下来,我们看一下如何将异常映射为某一个 HTTP 状态码。
异常一般会由 Spring 自身抛出,作为DispatcherServlet 处理过程中或执行校验时出现问题的结果。例如,如果DispatcherServlet 无法找到适合处理请求的控制器方法,那么将会抛出 NoSuchRequestHandlingMethodException 异常,最终的结果就是产生 404 状态码的响应( Not Found )。 尽管这些内置的映射是很有用的,但是对于应用所抛出的异常它们就无能为力了。幸好, Spring 提供了一种机制,能够通过 @ResponseStatus 注解将异常映射为 HTTP 状态码。
REST
以信息为中心的表述性状态转移( Representational State Transfer , REST )
- 表述性( Representational ): REST 资源实际上可以用各种形式来进行表述,包括 XML 、 JSON ( JavaScript Object Notation )甚至 HTML—— 最适合资源使用者的任意形式;
- 状态( State ):当使用 REST 的时候,我们更关注资源的状态而不是对资源采取的行为;
- 转移( Transfer ): REST 涉及到转移资源数据,它以某种表述性形式从一个应用转移到另一个应用。,
REST 就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来)。
Spring 支持以下方式来创建 REST 资源:
- @PathVariable 注解,控制器能够处理参数化的 URL; 借助 Spring 的视图和视图解析器,资源能够以多种方式进行表述,包括将模型数据渲染为 XML 、 JSON 、 Atom 以及 RSS 的 View 实现;
- @ResponseBody 注解和各种 HttpMethodConverter 实现,能够替换基于视图的渲染方式;
- @RequestBody 注解以及 HttpMethodConverter 实现可以将传入的 HTTP 数据转化为传入控制器处理方法的 Java 对象; 借助 RestTemplate , Spring 应用能够方便地使用 REST 资源。
Spring WebFlux
在Servlet3.1规范发布后,JavaEE就已经能够支持响应式的编程。Spring5发布了新一代响应式的Web框架,那便是Spring WebFlux。
基础概念
对于响应式框架,是基于响应式宣言的理念所产生的编程方式。响应式宣言分为4大理念。
- 灵敏的:就是可以快速响应的,只要有任何可能,系统都应该能够尽可能快地做出响应。
- 可恢复的:系统在运行中可能出现问题,但是能够有很强大的容错机制和修复机制保持响应性。
- 可伸缩的:在任何负载下,响应式编程都可以根据自身压力变化,请求少时,通过减少资源释放服务器的压力,负载大时能够通过扩展算法和软硬件的方式扩展服务能力,以经济实惠的方式实现可伸缩性。
- 消息驱动的:响应式编程存在异步消息机制,事件之间的协作是通过消息进行连接的。
基于这样的理念,响应式编程提出了各种模型来满足响应式编程的理念,其中著名的有Reactor和RxJava,Spring5就是基于它们构建WebFlux,而默认的情况下它会使用Reactor
Reactor 模型
传统的编程中的模型
在这个模型中往往是请求量大于系统最大线程数(这里记为M),假设大部分请求是比较耗时的操作,那么请求数量上来后,M条线程就不能及时地响应用户了。很显然,大量的线程就只能在请求队列中等待或者被系统所抛弃,这样对于后续的请求而言,要么新来的线程要等到旧线程运行完成后才能提供服务,要么就被系统所抛弃。这样的场景往往存在那种需要大量数据流的网站中,如视频、游戏、图片和复杂计算等的网站就常常发生这样的场景。
Reactor(反应器)模式
首先客户端会先向服务器注册其感兴趣的事件(Event),这样客户端就订阅了对应的事件,只是订阅事件并不会给服务器发送请求。当客户端发生一些已经注册的事件时,就会触发服务器的响应。当触发服务器响应时,服务器存在一个Selector线程,这个线程只是负责轮询客户端发送过来的事件,并不处理请求,当它接收到有客户端事件时,就会找到对应的请求处理器(Request Handler),然后启用另外一条线程运行处理器。因为Selector线程只是进行轮询,并不处理复杂的业务功能,所以它可以在轮询之后对请求做实时响应,速度十分快。由于事件存在很多种,所以请求处理器也存在多个,因此还需要进行区分事件的类型,所以Selector存在一个路由的问题。当请求处理器处理业务时,结果最终也会转换为数据流(data stream)发送到客户端。Reactor是一种基于事件的模型,对于服务器线程而言,它也是一种异步的,首先是Selector线程轮询到事件,然后通过路由找到处理器去运行对应的逻辑,处理器最后所返回的结果会转换为数据流。
Spring WebFlux的概述
在Servlet3.1之前,Web容器都是基于阻塞机制开发的,而在Servlet3.1(包含)之后,就开始了非阻塞的规范。对于高并发网站,使用函数式的编程就显得更为直观和简易,所以它十分适合那些需要高并发和大量请求的互联网的应用,特别是那些需要高速响应而对业务逻辑要求并不十分严格的网站,如游戏、视频、新闻浏览网站等。
在Spring WebFlux中,存在两种开发方式,一种是类似于Spring MVC的模式,另一种则是函数功能性的编程。
响应式编程分为Router Functions、Spring WebFlux和HTTP/Reactive Streams共3层。
- Router Functions:是一个路由分发层,也就是它会根据请求的事件,决定采用什么类的什么方法处理客户端发送过来的事件请求。显然,在Reactor模式中,它就是Selector的作用。
- Spring-webflux:是一种控制层,类似Spring MVC框架的层级,它主要处理业务逻辑前进行的封装和控制数据流返回格式等。
- HTTP/Reactive Streams:是将结果转换为数据流的过程。
Reactor提供的Flux和Mono
它们都是封装数据流的类。其中Flux是存放0N个数据流序列,响应式框架会一个接一个地(请注意不是一次性)将它们发送到客户端;而对于Mono则是存放01个数据流序列,这就是它们之间的区别,而它们是可以进行相互转换的。对于客户端,有时候响应能力距离服务端有很大的差距,如果在很短的时间内服务端将大量的数据流传输给客户端,那么客户端就可能被压垮。为了处理这个问题,一般会考虑使用响应式拉取,也就是将服务端的数据流划分为多个序列,一次仅发送一个数据流序列给客户端,当客户端处理完这个序列后,再给服务端发送消息,然后再拉取第二个序列进行处理,处理完后,再给服务端发送消息,以此类推,直至Flux中的0~N个数据流被完全处理,这样客户端就可以根据自己响应的速度来获取数据流。
WebHandler 接口
与Spring MVC使用DispactcherServlet不同的是Spring WebFlux使用的是WebHandler。它与DispatcherServlet 有异曲同工之妙,它们十分相似。WebHandler是一个接口,为此Spring WebFlux为其提供了几个实现类,以便于在不同的场景下使用。
运行流程
与Spring MVC 区别
可以看下Spring官方网站的对比图
WebFlux 是一个基本NIO实现的非阻塞web框架,支持大量并发连接。
Spring MVC 构建在Servlet APL之上,并使用同步阻塞I/O架构,每线程一个请求模型。
参考
Spring揭秘
Spring实战(第4版)
深入浅出Spring Boot 2.x