《Spring Boot 2.0 极简教程》附录 I : Spring 5.0 新特性

《Spring Boot 2.0 极简教程》附录 I : Spring 5.0 新特性

因为Spring Boot 2.0 是基于Spring Framework 5.0而开发的,所以我们这里对 Spring 5.0的新功能特性做一个简单的介绍。
.
Spring Framework 项目的领导人 Juergen Hoeller 于 2016 年 7 月 28 日宣布了第一个 Spring Framework 5.0 里程碑版本(5.0 M1)。2017 年 9 月 正式发行了Spring Framework 5.0 GA 版本。

Spring Framework 5.0 的亮点绝对是响应式编程,这是一个重要的范式转变。Spring Framework 5新引入的反应式(Reactor)编程框架WebFlux将会取代传统的基于Servlet API的阻塞模型。

虽然响应式编程是 Spring Framework 5.0 中的闪光点,但它不会在任何地方得到支持。 下游技术需要提供响应式支持。你会看到未来将要发布的 Spring Data、Spring Security、Spring Integration 等版本所提供的响应式编程功能。Spring Data 团队已经为 MongoDB 和 Redis 实现了响应式支持。使用 JDBC 获取响应式支持还为时过早。JDBC 规范本身就是阻塞的,在传统的 JDBC 数据库中看到响应式编程的还需要一段时间。

随着响应式编程越来越受欢迎,我们可以期待越来越多的技术将实现响应式解决方案。 Reactive Stack 在整个 Spring 5.0生态中的位置如下图所示

Spring 5 于 2017 年 9 月发布了通用版本 (GA),它标志着自 2013 年 12 月以来第一个主要 Spring Framework 版本。Spring 5 兼容 Java™8 和 JDK 9,它集成了反应式流 ( Reactive Stream ),提供一种颠覆性方法来实现Web 应用程序开发。

Spring WebFlux 是 Spring 5 的反应式编程的核心,它为开发人员提供了两种Spring Web编程模型:

 一种基于注解的模型
 Functional Web Framework (WebFlux.fn)

基于注解的模型是 Spring WebMVC 的现代替代方案,该模型基于反应式构建,而 Functional Web Framework 是传统的基于 @Controller 注解的编程模型的替代方案。

Spring Framework 5.0 的新特性如下:

基于JDK 8+ 和Java EE 7+
使用泛型类型推断、Lambda 表达式等提升代码可读性。
基于Java EE 7 API 支持:Servlet 3.1, Bean Validation 1.1, JPA 2.1, JMS 2.0
基于Web 服务器版本:Tomcat 8.5+, Jetty 9.4+, WildFly 10+
完全兼容JDK 9。
运行时兼容Java EE 8 API,Servlet 4.0, Bean Validation 2.0, JPA 2.2, JSON Binding API 1.0。
针对Tomcat 9.0, Hibernate Validator 6.0, Apache Johnzon 1.1进行测试。
在这里,我们举一个 Spring 5.0中使用 Java 8中Lambda 表达式的例子。在Java 8里,任何函数式接口作为方法的参数传入或者作为方法返回值的场合,都可以用Lambda表达式代替。例如,Spring的JdbcTemplate类里有一个方法定义如下:

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}

这个查询方法的第二个参数需要RowMapper接口的一个实例。在Java 8中我们可以写一个lambda表达式作为第二个参数的值传进去。
没有 Lambda 表达式之前,我们通常会把代码写成这样:

jdbcTemplate.query("select * from product", new RowMapper<Product>(){

  @Override
  public Product mapRow(ResultSet rs, int rowNum) throws SQLException {
    Integer id = rs.getInt("id");
    String description = rs.getString("description");
    Integer quantity = rs.getInt("quantity");
    BigDecimal price = rs.getBigDecimal("price");

    Product product = new Product();
    product.setId(id);
    product.setDescription(description);
    product.setQuantity(quantity);
    product.setPrice(price);

    return product;
  }});

而在 Java 8中,我们可以这么写:

jdbcTemplate.query("select * from product", (rs, rowNum) -> {
    Integer id = rs.getInt("id");
    String description = rs.getString("description");
    Integer quantity = rs.getInt("quantity");
    BigDecimal price = rs.getBigDecimal("price");

    Product product = new Product();
    product.setId(id);
    product.setDescription(description);
    product.setQuantity(quantity);
    product.setPrice(price);

    return product;
});

我们注意到Java 8中这段代码使用了lambda表达式,这比之前的版本中使用匿名内部类的方式紧凑、简洁得多。

移除的Packages, Classes 和Methods
移除包beans.factory.access (BeanFactoryLocator 机制).
移除包jdbc.support.nativejdbc (NativeJdbcExtractor 机制).
移除包mock.staticmock 从 spring-aspects 模块中移除。
移除包web.view.tiles2 和orm.hibernate3/hibernate4 .现在最低版本要求: Tiles 3 和Hibernate 5.
不再支持 AnnotationDrivenStaticEntityMockingControl

不再支持: Portlet, Velocity, JasperReports, XMLBeans, JDO, Guava.
建议: 如果上面的功能您仍需要使用,建议使用Spring Framework 4.3.x 。
通用基础包
基于JDK 8+ 的功能增强
兼容JDK 9
在包级别声明Non-null API:
可以显式使用 @Nullable 注解标注可空参数, 成员变量和返回值。
主要使用 IntelliJ IDEA 和Kotlin开发, 同时还有 Eclipse 与FindBugs.
一些Spring APIs 直接摒弃使用 null 值(e.g. StringUtils类)。
Resource 接口中readableChannel 的资源访问类基于 NIO实现 。
文件系统的访问类不再使用FileInput/OutputStream ,而是采用NIO.2 流。
Spring Framework 5.0 提供了自己的spring-jcl日志模块替代标准 Commons Logging,它还可以自动检测Log4j 2.x, SLF4J, JUL (java.util.logging),自带开箱即用的 Commons Logging 桥接器。Spring Logging 还提升了性能。
通过资源抽象支持防御性编程,为 getFile 访问提供了 isFile 指示器。
spring-core 模块中的修改字节码的功能包基于ASM 6.0。接下来会基于CGLIB 3.2.5 和Objenesis 2.6。

核心容器

支持 @Nullable 注解标注的optional injection points.
支持GenericApplicationContext和AnnotationConfigApplicationContext类的函数式风格编程。
在使用 CGLIB 代理下,对接口方法的事务、缓存和异步注释进行一致性检测。
将 XML 配置命名空间简化为无版本化的模式。
始终使用最新的 xsd 文件;不再支持已弃用的功能。
仍然支持声明特定版本, 但针对最新的schema进行了验证。
支持候选组件的索引 (作为类路径扫描的替代解决方案)。Spring Framework 5 改进了扫描和识别组件的方法,使大型项目的性能得到提升。目前,扫描是在编译时执行的,而且向 META-INF/spring.components 文件中的索引文件添加了组件坐标。该索引是通过一个为项目定义的特定平台应用的构建任务来生成的。标有来自 javax 包的注解的组件会添加到索引中,任何带 @Index 注解的类或接口都会添加到索引中。
从索引读取实体而不是全量扫描类路径, 在性能上, 对于小于 200 个类的小型项目可能没有明显差异,但对大型项目影响较大: 加载组件索引开销更低。加载组件索引的耗费是廉价的。因此当类的数量不断增长,加上构建索引的启动时间仍然可以维持一个常数; 而对于组件扫描而言,启动时间则会有明显的增长。这个对于我们处于大型 Spring 项目的开发者所意味着的,是应用程序的启动时间将被大大缩减。虽然 20 或者 30 秒钟看似没什么,但如果每天要这样登上好几百次,加起来就够你受的了。使用了组件索引能帮助您更加快速地启动 Spring 应用,节省了宝贵的时间。
Spring 的传统类路径扫描方式没有删除,而是保留为一种后备选择。

Spring Web MVC

在 Spring 提供的过滤器接口 Filter实现中完整支持Servlet 3.1。
支持 Spring MVC 控制器方法中的 Servlet 4.0 PushBuilder 参数。
MaxUploadSizeExceededException 共用服务器上的 Servlet 3.0 多部分解析。
通过 MediaTypeFactory 委派统一支持公共媒体类型。
取代使用 Java Activation框架。
与不可变对象的数据绑定 (Kotlin / Lombok / @ConstructorProperties)
支持 JSON 绑定 API (使用 Eclipse Yasson 或 Apache Johnzon 作为Jackson和 GSON 的替代品)。
支持Jackson 2.9。
支持 Protobuf 3。
支持Reactor 3.1 Flux 和 Mono 以及 RxJava 1.3/2.1 作为spring mvc 控制器的方法返回值, 目标是在Spring MVC controllers中使用新的reactive WebClient 或Spring Data Reactive Repositories。
新的 ParsingPathMatcher 替代 AntPathMatcher, 具有更高效的解析和扩展语法。
@ExceptionHandler 方法允许传入 RedirectAttributes 参数。
支持 ResponseStatusException 作为 @ResponseStatus 的可选替代方案。
不需要再去实现Invocable 接口来创建脚本执行引擎, 可以直接使用 ScriptEngine#eval(String, Bindings) 来执行脚本, 并通过再在 ScriptTemplateView中传递 RenderingContext参数来实现 i18n 国际化和嵌套模板。
·Spring's FreeMarker宏(macros)spring.ftl 现在使用 HTML 输出格式 (需要 FreeMarker 2.3. 24 + 版本支持)。

Spring WebFlux

Spring 5.0的一个激动人心的特性就是新的响应式 Web框架 WebFlux, 它是完全响应式且非阻塞的。Reactive Streams 是来自于 Netflix, Pivotal, Typesafe, Red Hat, Oracle, Twitter 以及 Spray.io 的工程师们特地开发的API框架。它为响应式编程实现的实现提供了一个公共的 API。就好比是实现JPA接口的Hibernate 框架。这里 JPA 就是这个Reactive Streams API, 而 Hibernate 就是其具体实现。Reactive Streams API 是 Java 9 的官方版本的一部分。在 Java 8 中, 需要专门引入依赖来使用 Reactive Streams API。

Spring Framework 5.0 中拥有一个新的 spring-webflux 模块,支持响应式 HTTP 和 WebSocket 客户端。Spring Framework 5.0 还提供了对于运行于服务器之上,包含了 REST, HTML, 以及 WebSocket 风格交互的响应式 Web应用程序的支持。

在 spring-webflux 中包含了两种独立的服务端编程模型:

(1)基于注解:使用到了@Controller 以及 Spring MVC 的其它一些注解;
(2)使用 Java 8 lambda 表达式的函数式风格的路由和处理。
有了 Spring Webflux, 现在可以创建出响应式且非阻塞的WebClient作为 RestTemplate 的一个替代方案。

下面是一个使用 Spring 5.0 的 REST 端点的 WebClient 实现示例:

WebClient webClient = WebClient.create();
Mono person = webClient.get()
.uri("http://localhost:8080/movie/42")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.then(response -> response.bodyToMono(Movie.class));
WebClient webClient = WebClient.create();
Mono person = webClient.get()
.uri("http://localhost:8080/movie/42")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.then(response -> response.bodyToMono(Movie.class));

尽管新的 WebFlux 模块给我么带来了激动人心的新能力,传统的 Spring MVC 在 Spring Framework 5.0 仍然得到了完整的支持。

使用 Kotlin 进行函数式编程

Spring Framework 5.0 引入了对 JetBrains Kotlin 语言的支持。同时,Spring 5.0中也在使用 Kotlin 语言进行开发 API。我们可以到spring-framework源码工程目录下面去看一下 Kotlin 的代码列表:

spring-framework$ find . -name *.kt
./spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanFactoryExtensions.kt
./spring-beans/src/main/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensions.kt
./spring-beans/src/test/kotlin/org/springframework/beans/BeanUtilsKotlinTests.kt
./spring-beans/src/test/kotlin/org/springframework/beans/factory/annotation/KotlinAutowiredTests.kt
./spring-beans/src/test/kotlin/org/springframework/beans/factory/BeanFactoryExtensionsTests.kt
./spring-beans/src/test/kotlin/org/springframework/beans/factory/ListableBeanFactoryExtensionsTests.kt
./spring-context/src/main/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensions.kt
./spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt
./spring-context/src/main/kotlin/org/springframework/context/support/GenericApplicationContextExtensions.kt
./spring-context/src/main/kotlin/org/springframework/ui/ModelExtensions.kt
./spring-context/src/main/kotlin/org/springframework/ui/ModelMapExtensions.kt
./spring-context/src/test/kotlin/org/springframework/context/annotation/AnnotationConfigApplicationContextExtensionsTests.kt
./spring-context/src/test/kotlin/org/springframework/context/annotation/Spr16022Tests.kt
./spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt
./spring-context/src/test/kotlin/org/springframework/context/support/GenericApplicationContextExtensionsTests.kt
./spring-context/src/test/kotlin/org/springframework/ui/ModelExtensionsTests.kt
./spring-context/src/test/kotlin/org/springframework/ui/ModelMapExtensionsTests.kt
./spring-core/src/main/kotlin/PropertyResolverExtensions.kt
./spring-core/src/test/kotlin/org/springframework/core/env/PropertyResolverExtensionsTests.kt
./spring-core/src/test/kotlin/org/springframework/core/KotlinMethodParameterTests.kt
./spring-core/src/test/kotlin/org/springframework/core/KotlinReflectionParameterNameDiscovererTests.kt
./spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt
./spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensions.kt
./spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensionsTests.kt
./spring-jdbc/src/test/kotlin/org/springframework/jdbc/core/namedparam/MapSqlParameterSourceExtensionsTests.kt
./spring-messaging/src/test/kotlin/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerKotlinTests.kt
./spring-test/src/main/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensions.kt
./spring-test/src/test/kotlin/org/springframework/test/web/reactive/server/WebTestClientExtensionsTests.kt
./spring-web/src/main/kotlin/org/springframework/web/client/RestOperationsExtensions.kt
./spring-web/src/test/kotlin/org/springframework/web/client/RestOperationsExtensionsTests.kt
./spring-web/src/test/kotlin/org/springframework/web/method/annotation/RequestParamMethodArgumentResolverKotlinTests.kt
./spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensions.kt
./spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt
./spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt
./spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensions.kt
./spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/ClientResponseExtensionsTests.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerRequestExtensionsTests.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverKotlinTests.kt
./spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/view/script/ScriptTemplateWithBindingsExtensions.kt
./spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodKotlinTests.kt
./spring-webmvc/src/test/kotlin/org/springframework/web/servlet/view/script/ScriptTemplateWithBindingsExtensions.kt

我们可以来阅读以下下面这个类的代码,来看看 Spring 框架是怎样使用 Kotlin 中的 DSL 实现极简化的编程风格的。

spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt

这个类的代码如下:

package org.springframework.context.support

import org.springframework.beans.factory.config.BeanDefinitionCustomizer
import org.springframework.beans.factory.support.AbstractBeanDefinition
import org.springframework.context.ApplicationContextInitializer
import org.springframework.core.env.ConfigurableEnvironment
import java.util.function.Supplier

fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
    val beans = BeanDefinitionDsl()
    beans.init()
    return beans
}

open class BeanDefinitionDsl(private val condition: (ConfigurableEnvironment) -> Boolean = { true })
    : ApplicationContextInitializer<GenericApplicationContext> {
    @PublishedApi
    internal val registrations = arrayListOf<(GenericApplicationContext) -> Unit>()
    @PublishedApi
    internal val children = arrayListOf<BeanDefinitionDsl>()

    enum class Scope {
        SINGLETON,
        PROTOTYPE
    }

    enum class Autowire {
        NO,
        BY_NAME,
        BY_TYPE,
        CONSTRUCTOR
    }

    inner class BeanDefinitionContext(@PublishedApi internal val context: GenericApplicationContext) {
        inline fun <reified T : Any> ref(name: String? = null): T = when (name) {
            null -> context.getBean(T::class.java)
            else -> context.getBean(name, T::class.java)
        }

        val env: ConfigurableEnvironment
            get() = context.environment
    }

    inline fun <reified T : Any> bean(name: String? = null,
                                      scope: Scope? = null,
                                      isLazyInit: Boolean? = null,
                                      isPrimary: Boolean? = null,
                                      autowireMode: Autowire = Autowire.CONSTRUCTOR,
                                      isAutowireCandidate: Boolean? = null) {
        registrations.add {
            val customizer = BeanDefinitionCustomizer { bd ->
                scope?.let { bd.scope = scope.name.toLowerCase() }
                isLazyInit?.let { bd.isLazyInit = isLazyInit }
                isPrimary?.let { bd.isPrimary = isPrimary }
                isAutowireCandidate?.let { bd.isAutowireCandidate = isAutowireCandidate }
                if (bd is AbstractBeanDefinition) {
                    bd.autowireMode = autowireMode.ordinal
                }
            }
            when (name) {
                null -> it.registerBean(T::class.java, customizer)
                else -> it.registerBean(name, T::class.java, customizer)
            }
        }
    }

    inline fun <reified T : Any> bean(name: String? = null,
                                      scope: Scope? = null,
                                      isLazyInit: Boolean? = null,
                                      isPrimary: Boolean? = null,
                                      autowireMode: Autowire = Autowire.NO,
                                      isAutowireCandidate: Boolean? = null,
                                      crossinline function: BeanDefinitionContext.() -> T) {
        val customizer = BeanDefinitionCustomizer { bd ->
            scope?.let { bd.scope = scope.name.toLowerCase() }
            isLazyInit?.let { bd.isLazyInit = isLazyInit }
            isPrimary?.let { bd.isPrimary = isPrimary }
            isAutowireCandidate?.let { bd.isAutowireCandidate = isAutowireCandidate }
            if (bd is AbstractBeanDefinition) {
                bd.autowireMode = autowireMode.ordinal
            }
        }
        registrations.add {
            val beanContext = BeanDefinitionContext(it)
            when (name) {
                null -> it.registerBean(T::class.java,
                        Supplier { function.invoke(beanContext) }, customizer)
                else -> it.registerBean(name, T::class.java,
                        Supplier { function.invoke(beanContext) }, customizer)
            }
        }
    }

    fun profile(profile: String, init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
        val beans = BeanDefinitionDsl({ it.activeProfiles.contains(profile) })
        beans.init()
        children.add(beans)
        return beans
    }

    fun environment(condition: ConfigurableEnvironment.() -> Boolean,
                    init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
        val beans = BeanDefinitionDsl(condition::invoke)
        beans.init()
        children.add(beans)
        return beans
    }

    override fun initialize(context: GenericApplicationContext) {
        for (registration in registrations) {
            if (condition.invoke(context.environment)) {
                registration.invoke(context)
            }
        }
        for (child in children) {
            child.initialize(context)
        }
    }
}

其中的

fun beans(init: BeanDefinitionDsl.() -> Unit): BeanDefinitionDsl {
    val beans = BeanDefinitionDsl()
    beans.init()
    return beans
}

这段代码就是 Kotlin 中实现DSL的典型例子。关于 Kotlin DSL 的相关内容可以参考《Kotlin 极简教程》一书。
有了这个扩展类,我们就可以使用Kotlin DSL 风格的函数式Bean 定义


beans {
    bean<UserHandler>()
    bean<Routes>()
    bean<WebHandler>("webHandler") {
    RouterFunctions.toWebHandler(
        ref<Routes>().router(),
        HandlerStrategies.builder().viewResolver(ref()).build())
    }
    bean("messageSource") {
        ReloadableResourceBundleMessageSource().apply {
            setBasename("messages")
            setDefaultEncoding("UTF-8")
        }
    }
    bean {
        val prefix = "classpath:/templates/"
        val suffix = ".mustache"
        val loader = MustacheResourceTemplateLoader(prefix, suffix)
        MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply {
            setPrefix(prefix)
            setSuffix(suffix)
        }
    }
    profile("foo") {
        bean<Foo>()
    }
 }

Kotlin 是一种支持函数式编程编程风格的面向对象语言。Kotlin 运行在 JVM 之上,但运行环境并不限于 JVM。有了对 Kotlin 的支持,开发者可以进行深度的函数式 Spring 编程,特别是在函数式 Web 端点以及 Bean 注册这些方面。

在 Spring Framework 5.0 中, 可以为 WEB 的函数式 API 编写干净极简而且相当“地道”的 Kotlin 代码,就像下面这样:

{
("/movie" and accept(TEXT_HTML)).nest {
GET("/", movieHandler::findAllView)
GET("/{card}", movieHandler::findOneView)
}
("/api/movie" and accept(APPLICATION_JSON)).nest {
GET("/", movieApiHandler::findAll)
GET("/{id}", movieApiHandler::findOne)
}
}

对于 Bean 的注册,作为 XML 或者 @Configuration 以及 @Bean 的替代办法, 现在你可以使用 Kotlin 来注册 Spring Bean了,就像下面这样:

val context = GenericApplicationContext {
registerBean()
registerBean { Cinema(it.getBean()) }
}

测试方面的提升

Spring Framework 5.0 完全支持 JUnit 5 Jupiter,所以可以使用 JUnit 5 来编写测试以及扩展。此外还提供了一个编程以及扩展模型,Jupiter 子项目提供了一个测试引擎来在 Spring 上运行基于 Jupiter 的测试。另外,Spring Framework 5 还提供了在 Spring TestContext Framework 中进行并行测试的扩展。
现在可以在您的单元测试中利用 Java 8 中提供的函数式编程特性。下面的代码清单演示了这一支持:
代码清单 JUnit 5 全面使用了 Java 8 流和 Lambda 表达式

@Test
void givenStreamOfInts_SumShouldBeMoreThan_100() {
    assertTrue(Stream.of(10, 50, 50)
      .stream()
      .mapToInt(i -> i)
      .sum() > 110, () -> "序列之和大于 100");
}

Spring 5 继承了 JUnit 5,在 Spring TestContext Framework 内实现了多个扩展的 API。针对响应式编程模型, spring-test 现在还引入了支持 Spring WebFlux 的 WebTestClient 集成测试的支持,类似于 MockMvc,并不需要一个运行着的服务端。使用一个模拟的请求或者响应, WebTestClient 就可以直接绑定到 WebFlux 服务端设施。
WebTestClient 可绑定到真实的服务器,或者使用控制器或函数。在下面的代码清单中,WebTestClient 被绑定到 localhost:

代码清单. 绑定到 localhost 的 WebTestClient

WebTestClient testClient = WebTestClient
  .bindToServer()
  .baseUrl("http://localhost:8080")
  .build();

在下面的代码清单中,测试了 RouterFunction:

代码清单. 将 WebTestClient 绑定到 RouterFunction

RouterFunction bookRouter = RouterFunctions.route(
  RequestPredicates.GET("/books"),
  request -> ServerResponse.ok().build()
);
  
WebTestClient
  .bindToRouterFunction(bookRouter)
  .build().get().uri("/books")
  .exchange()
  .expectStatus().isOk()
  .expectBody().isEmpty();

当然, Spring Framework 5.0 仍然支持我们的老朋友 Junit。JUnit 5 现在是GA 版本。对于 JUnit4, Spring Framework 在未来还是要支持一段时间的。

库支持
Spring Framework 5.0目前支持以下升级库的版本 :

Jackson 2.6+
EhCache 2.10+ / 3.0 GA
Hibernate 5.0+
JDBC 4.0+
XmlUnit 2.x+
OkHttp 3.x+
Netty 4.1+

Spring 5.0 详细的新特性可参考Github wiki:https://github.com/spring-projects/spring-framework/wiki/What's-New-in-Spring-Framework-5.x

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容