Spring Boot 学习(2)

时间:2020-12-02 08:06:05

文 by / 林本托

Tips

做一个终身学习的人。

Spring Boot 学习(2)

源代码:github下的/code01/ch2。

配置 Web 应用程序

在上一章中,我们学习了如何创建一个基本的应用程序模板,并添加了一些基本功能,并建立与数据库的连接。 在本章中,我们将继续增强BookPub应用程序,并提供 Web 支持。

在本章,主要包括以下内容:

  • 创建一个基本的 RESTful 风格的应用程序;
  • 创建一个 Spring Data REST 服务;
  • 配置一个自定义的 Servlet 的过滤器;
  • 配置一个自定义的拦截器;
  • 配置一个HttpMessageConverters的转换器;
  • 配置一个自定义的PropertyEditors编辑器;
  • 配置一个自定义的类型格式化类。

一. 创建一个基本的 RESTful 风格的应用程序

虽然命令行应用程序确实有其用途,但今天的大多数应用程序开发都围绕着 Web,REST 和数据服务。 我们开始增强 BookPub 应用程序,提供一个基于Web 的 API,以便访问图书目录。

我们继续使用前一章创建的应用程序框架,其中定义了实体对象和存储库服务,并配置了与数据库的连接。

首先,第一件事情是我们需要在build.gradle 文件中添加新的依赖模块spring-boot-starter-web,以便获取基于 web 服务的所需的类库。具体的代码片段如下:

dependencies {
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-starter-jdbc")
compile("org.springframework.boot:spring-boot-starter-web")
runtime("com.h2database:h2")
testCompile("org.springframework.boot:spring-boot-
starter-test")
}

接下来,创建一个 Spring 控制器,用于处理我们应用程序中获取目录数据的 Web 请求。 新建一个包目录来存放控制器相关的 Java 程序,以便我们的代码按照适当的目的分组。 在 src/main/java/org/test/bookpub 目录下创建一个名为 controllers的包目录。

因为需要暴露图书的数据,所以,在新建的包下,创建一个控制器类BookController

package org.test.bookpub.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.test.bookpub.entity.Book;
import org.test.bookpub.entity.Reviewer;
import org.test.bookpub.repository.BookRepository; import java.util.Collections;
import java.util.List; @RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookRepository bookRepository; @RequestMapping(value = "", method = RequestMethod.GET)
public Iterable<Book> getAllBooks() {
return bookRepository.findAll();
} @RequestMapping(value = "/{isbn}", method =
RequestMethod.GET)
public Book getBook(@PathVariable String isbn) {
return bookRepository.findBookByIsbn(isbn);
}
}

然后, 使用 ./gradlew clean bootRun.命令启动应用程序。

最后,当应用程序程序启动以后,在浏览器中输入:http://localhost:8080/books, 然后会在页面上显示“[]”,表示目前没有图书的数据。

获取暴露于Web请求的服务的关键是@RestController注解。这是一个元注解使用的例子,正如Spring文档指出的那样,我们在以前的代码看到过。 在@RestController中,定义了两个注解:@Controller和@ResponseBody。 所以我们可以轻松给BookController类添加注解,如下所示:

@Controller
@ResponseBody
@RequestMapping("/books")
public class BookController {...}

@Controller是一个类似于@Bean 和@Repository的Spring的元注解,并将已经添加注解的类声明为 MVC 控制器。

@ResponseBody是一个Spring MVC的注解,指示来自Web请求映射方法的响应,构成HTTP响应主体有效负载的整个内容,这是RESTful应用程序比较典型的使用场景。

二. 创建一个 Spring Data REST 服务

在上一个例子中,我们使用REST控制器来展示我们的BookRepository,通过Web RESTful API访问后台的数据。 虽然这是数据访问的一种快捷方便的方式,但它要求我们手动创建一个控制器并定义所有所需操作的映射。 为了最小化代码,Spring为我们提供了一种更方便的方法:spring-boot-starter-data-rest模块。 这允许我们简单地向存储库接口添加一个注解,而Spring将做剩下的事情用以将数据暴露给Web。

首先,我们需要在build.gradle文件中添加spring-boot-starter-data-rest模块。

dependencies {
...
compile("org.springframework.boot:spring-boot-starter-data-rest")
...
}

第二步,在src/main/java/org/test/bookpub/repository目录下新建AuthorRepository接口,代码如下:

package org.test.bookpub.repository;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.stereotype.Repository;
import org.test.bookpub.entity.Author; @RepositoryRestResource
public interface AuthorRepository extends PagingAndSortingRepository<Author, Long> {
}

接下来为剩下的实体模型创建对应的接口,

package org.test.bookpub.repository;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.test.bookpub.entity.Publisher; @RepositoryRestResource
public interface PublisherRepository extends PagingAndSortingRepository<Publisher, Long> {
}
package org.test.bookpub.repository;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.test.bookpub.entity.Reviewer; @RepositoryRestResource
public interface ReviewerRepository extends PagingAndSortingRepository<Reviewer, Long> {
}

代码完成后,执行./gradlew clean bootRun。当启动成功以后,访问http://localhost:8080/profile/authors,在 Chrome 浏览器下,显示如下内容:

{
"_embedded" : {
"authors" : [ ]
},
"_links" : {
"self" : {
"href" : "http://localhost:8080/authors{?page,size,sort}",
"templated" : true
},
"profile" : {
"href" : "http://localhost:8080/profile/authors"
}
},
"page" : {
"size" : 20,
"totalElements" : 0,
"totalPages" : 0,
"number" : 0
}
}

从浏览器里显示的内容可以看出,我们将获得比我们编写BookController控制器更多的信息。 之所以显示了更多的信息,由于我们没有扩展CrudRepository接口,而是扩展了PagingAndSortingRepository,而它又是CrudRepository的扩展。 这样做的原因是获得PagingAndSortingRepository提供的额外好处。 这将添加额外的功能,使用分页检索实体,并能够对它们进行排序。

@RepositoryRestResource注解是可选项,但可以让我们更好地控制作为Web数据服务的存储库的暴露。 例如,如果我们要将URL路径由“authers”改成“rel”,则可以此注解调整,如下所示:

@RepositoryRestResource(collectionResourceRel = "writers", path = "writers")

修改以后,之前的 url 现在改完“http://localhost:8080/reviewers”。

由于我们在构建依赖项中包含spring-boot-starter-data-rest模块,我们还获得spring-hateoas类库的支持,此库给我们提供了很好的ALPS元数据,比如_links对象。 这在构建API驱动的UI时可能非常有用,这可以从元数据推导出导航功能并以适合的方式呈现它们。

Tips

关于更多 ALPS 元数据的信息,请参考https://spring.io/blog/2014/07/14/spring-data-rest-now-comes-with-alps-metadata。

三. 配置一个自定义的 Servlet 的过滤器

在一个真实的Web应用程序中,我们几乎总是需要为服务请求添加装饰器或包装器,用来记录它们,过滤XSS(跨站脚本攻击)的非法字符,执行认证等。Spring Boot自动添加OrderedCharacterEncodingFilter和HiddenHttpMethodFilter 两个过滤器,除此之外,还有其他的过滤器。 让我们看看Spring Boot如何帮助我们实现这个任务。

在Spring Boot,Spring Web,Spring MVC等的各种框架中,已经有各种不同的servlet过滤器可用,我们所要做的就是将它们作为一个bea定义到配置中。 假设应用程序将在负载平衡器代理之后运行,并且当我们的应用程序实例收到请求时,我们希望将用户使用的实际请求IP转换为代理的IP。 幸运的是,Tomcat 8已经为我们提供了一个实现:RemoteIpFilter。 我们需要做的就是将其添加到我们的过滤器链中。

根据功能的不同,便于管理和职责清晰,我们需要把不同的类放在不同的包下,我们创建一个WebConfiguration.java 的文件,放在src/main/java/org/test/bookpub 目录下。

package org.test.bookpub;

import org.apache.catalina.filters.RemoteIpFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Bean
public RemoteIpFilter remoteIpFilter() {
return new RemoteIpFilter();
}
}

第二步,执行./gradlew clean bootRun命令,在启动中,查看 log,会出现以下信息,表示过滤器已经添加:

o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'remoteIpFilter' to: [/*]

这个功能背后的功能其实很简单。 我们从单独的配置类开始,并且将我们的方法用于过滤器bean的检测。

我们看看主类BookPubApplication,这个类添加了@SpringBootApplication注解,此注解是是一个元注解,它声明了@ComponentScan等其他注解。@ComponentScan的存在指示Spring Boot将WebConfiguration类检测为@Configuration注解的标记类,并将其定义添加到应用程序上下文中。所以,我们将在WebConfiguration类中声明的任何事情就好像我们将它放在BookPubApplication类中是一样的。

@Bean public RemoteIpFilter remoteIpFilter(){...}声明为RemoteIpFilter类创建一个Spring bean。当Spring Boot 检测到javax.servlet.Filter的所有bean时,它将自动将它们添加到过滤器链中。所以我们要做的就是,如果要添加更多的过滤器,那就是将它们声明为@Bean配置。例如,对于更高级的过滤器配置,如果希望特定的过滤器仅适用于特定的URL模式,可以创建一个FilterBistrationBean类型的@Bean配置,并用来配置精确的设置。

四. 配置一个自定义的拦截器

Servlet过滤器是Servlet API的一部分,与Spring完全没有任何关系,除了自动添加到过滤器链中,Spring MVC为我们提供了另一种方式来包装Web请求:HandlerInterceptor拦截器。根据文档,HandlerInterceptor就像一个Filter,但是,拦截器不是在嵌套链中包含请求,而是在处理请求、处理视图或在页面渲染之前,在不同的阶段向我们提供了拦截点,对请求进行拦截。到最后,请求已经完成。它不改变有关请求的任何内容,但是如果拦截器逻辑返回false,它允许我们通过抛出异常来停止执行。

与过滤器的情况类似,Spring MVC附带了一些预定义的HandlerInterceptor。常用的是LocaleChangeInterceptor和ThemeChangeInterceptor。接下来在应用程序中添加LocaleChangeInterceptor,看看它是如何完成的。

添加一个拦截器并不像刚才声明一个bean那么简单。 实际上需要通过实现WebMvcConfigurer接口或重写WebMvcConfigurationSupport来实现。

第一步,WebConfiguration类继承WebMvcConfigurerAdapter类。

public class WebConfiguration extends
WebMvcConfigurerAdapter {…}

接下来,为LocaleChangeInterceptor拦截器增加@Bean注解,

@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
return new LocaleChangeInterceptor();
}

这实际上只会创建拦截 Spring bean,但不会将其添加到请求处理链中。 为了实现这一点,我们需要重写addInterceptors方法,注册拦截器。

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}

整个代码如下:

package org.test.bookpub;

import org.apache.catalina.filters.RemoteIpFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Bean
public RemoteIpFilter remoteIpFilter() {
return new RemoteIpFilter();
} @Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
return new LocaleChangeInterceptor();
} @Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}

执行 ./gradlew clean bootRun

应用程序启动以后,在浏览器中输入http://localhost:8080/books?locale=foo。

这时,浏览器报错:

Spring Boot 学习(2)

后台的错误 log 如下:

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution strategy

Tips

上面的错误不是因为我们输入了无效的区域设置,而是因为默认语言环境解析策略不允许重置浏览器请求的语言环境。出现一个错误,实际上是证明了拦截器已经生效了。

当涉及到配置Spring MVC内部组件时,它并不像只是定义一堆bean那样简单—— 至少不总是这样。 这是因为需要向请求提供更精细的MVC组件映射。 为了简化难度,Spring为我们提供了WebMvcConfigurerAdapter适配器,它是WebMvcConfigurer接口实现,我们可以扩展和覆盖我们需要的设置。

在配置拦截器的特定情况下,我们可以重写addInterceptors(InterceptorRegistry registry)方法。 这是一个典型的回调方法,我们给予一个注册类,以便根据需要注册多个附加拦截器。 在MVC自动配置阶段,Spring Boot就像过滤器一样检测到WebMvcConfigurer的实例,并依次调用所有这些回调方法。 这意味着如果需要其他逻辑分离,可以有多个WebMvcConfigurer类的实现。

五. 配置一个HttpMessageConverters转换器

在构建RESTful Web服务时,我们定义了控制器,资源库,并在其上面添加了注解,但是从Java实体bean到HTTP数据流输出没有任何类型的对象转换。 实际上,Spring Boot自动配置了HttpMessageConverters转换器将实体bean对象转换为使用了Jackson类库的JSON的格式,将生成的JSON数据写入HTTP响应输出流。 当多个转换器可用时,根据消息对象类和请求的内容类型选择最适用的转换器。

HttpMessageConverters的目的是将各种对象类型转换为相应的HTTP输出格式。 转换器可以支持一系列多种数据类型或多种输出格式,或两者的组合。 例如,MappingJackson2HttpMessageConverter类可以将任何Java对象转换为application/json的格式,而ProtobufHttpMessageConverter类只能对com.google.protobuf.Message的实例进行操作,但可以将其作为application/json,application/xml,text/plain或application/xprotobuf格式。 HttpMessageConverters不仅支持写入HTTP流,还支持将HTTP请求转换为适当的Java对象。

我们可以通过多种方式配置转换器。 这一切都取决于你喜欢哪一个,或者想要实现多少控制。

首先,我们在WebConfiguration类中增加ByteArrayHttpMessageConverter,并加上@Bean注解。

@Bean
public
ByteArrayHttpMessageConverter byteArrayHttpMessageConverter() {
return new ByteArrayHttpMessageConverter();
}

另一种实现方式是重写WebConfiguration类中的configureMessageConverters方法,首先需要继承WebMvcConfigurerAdapter类,具体的代码如下:

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ByteArrayHttpMessageConverter());
}

如果想获取更多的控制,还可以重写extendMessageConverters方法。

@Override
public void
extendMessageConverters(List<HttpMessageConverter<?>>
converters) {
converters.clear();
converters.add(new ByteArrayHttpMessageConverter());
}

如上所示,Spring给了我们多种方式来实现同样的事情,这一切都取决于我们的偏好或具体的实现细节。

我们介绍了将HttpMessageConverter添加到应用程序中的三种不同的方法。 那有什么区别呢?

将HttpMessageConverter声明为@Bean是向应用程序添加自定义转换器的最快捷,最简单的方法。 它类似于我们在前面的例子中添加了Servlet过滤器。 如果Spring检测到一个HttpMessageConverter类型的bean,它将自动将其添加到列表中。 如果WebConfiguration类没有继承WebMvcConfigurerAdapter父类,那么这是首选方法。

当应用程序需要指定WebMvcConfigurerAdapter的扩展以配置其他的东西,如拦截器,那么重写configureMessageConverters方法并将我们的转换器添加到列表将更为协调一致。可以从Spring Boot 的模块中添加多个WebMvcConfigurers实例并自动配置,但是不能保证我们的方法可以以任何特定的顺序被调用。

如果我们需要做一些更加具体的事情,比如从列表中删除所有其他转换器或清除重复的转换器,这需要重写extendMessageConverters方法的地方。 所有WebMvcConfigurer被调用到configureMessageConverter方法并且转换器列表被完全填充后调用此方法。 当然,WebMvcConfigurer的其他一些实例完全可以重写extendMessageConverters, 但是这样做的机会并不多。

六. 配置一个自定义的PropertyEditors编辑器

在前面的例子中,我们学习了如何为HTTP请求和响应数据配置转换器。 还有其他类型的转换,特别是在将参数动态转换为各种对象时,例如String类型转换为Date或Integer。

当我们在控制器中声明一个映射方法时,Spring使用确切的对象类型来*定义方法签名。 这个方式是通过使用PropertyEditor实现的。 PropertyEditor是一个默认概念,定义为JDK的一部分,旨在允许将文本值转换为给定类型。 它最初用于构建Java Swing / AWT GUI,后来被证明是适合Spring需要将Web参数转换为方法参数类型的需要。

Spring MVC已经为很多常见类型(如布尔型,货币型和类)提供了大量的PropertyEditor实现。 假设我们要创建一个Isbn类对象,并在我们的控制器中使用它,而不是一个纯粹的String类型。

首先,我们需要在WebConfiguration类中移除extendMessageConverters方法,因为调用converters.clear()这段代码会中断渲染,因为删除了所有支持的类型转换器。

然后,定义Isbn类,和对应的IsbnEditor属性编辑器,以及重写initBinde方法给我们的BookController类,使用以下内容配置IsbnEditor

public class Isbn {
private String isbn; public Isbn(String isbn) {
this.isbn = isbn;
} public String getIsbn() {
return isbn;
}
}
public class IsbnEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text)) {
setValue(new Isbn(text.trim()));
}
else {
setValue(null);
}
} @Override
public String getAsText() {
Isbn isbn = (Isbn) getValue();
if (isbn != null) {
return isbn.getIsbn();
}
else {
return "";
}
}
} @InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Isbn.class, new IsbnEditor());
}

第三步,在BookController类中修改getBook方法,以便可以接受 Isbn类型的对象,

@RequestMapping(value = "/{isbn}", method = RequestMethod.GET)
public Book getBook(@PathVariable Isbn isbn) {
return bookRepository.findBookByIsbn(isbn.getIsbn());
}

第四步,启动./gradlew clean bootRun,启动成功以后,在浏览器中输入http://localhost:8080/books/978-1-78528-415-1。

虽然我们不会观察到任何可见的更改,但IsbnEditor确实在工作,从{isbn}参数中创建Isbn类对象实例。我们打印了传递过来的Isbn实例,重写了toString()方法。

Spring Boot 学习(2)

Spring自动配置大量的默认编辑器,但是对于自定义类型,我们必须明确地为每个Web请求实例化新的编辑器。 这是在控制器中使用@InitBinder注解的方法完成的。 扫描此注解,所有检测到的方法应具有接受WebDataBinder作为参数的签名。 除此之外,WebDataBinder还为我们提供了注册尽可能多的自定义编辑器的能力,要求控制器的方法被正确绑定。

Tips

PropertyEditor不是线程安全的!

因此,我们必须为每个Web请求创建一个新的自定义编辑器实例,并将其注册到WebDataBinder。

如果需要新的PropertyEditor,最好通过扩展PropertyEditorSupport类并自定义重写所需的方法来创建。

七. 配置一个自定义的类型格式化类

PropertyEditor因为它的状态和非线程安全,从版本3起,Spring添加了一个Formatter接口作为PropertyEditor的替代。 格式化类旨在提供类似的功能,但是以完全线程安全的方式,并专注于解析对象类型中的String并将对象转换为其字符串表示形式的非常具体的任务。

对于我们的应用程序,希望有一个格式化程序可以使用一个字符串形式的书籍的ISBN号码并将其转换为一个Book实体对象。 这样,当请求URL签名仅包含ISBN号码或数据库ID时,就可以使用Book类型的参数定义控制器请求的方法。

首先,在src/main/java/org/test/bookpub目录下创建一个新的包formatters,在此包下,创建BookFormatter类并实现Formatter接口,代码示例如下:

public class BookFormatter implements Formatter<Book> {
private BookRepository repository;
public BookFormatter(BookRepository repository) {
this.repository = repository;
}
@Override
public Book parse(String bookIdentifier, Locale locale) throws ParseException {
Book book = repository.findBookByIsbn(bookIdentifier);
return book != null ? book : repository.findOne(Long.valueOf(bookIdentifier));
}
@Override
public String print(Book book, Locale locale) {
return book.getIsbn();
}
}

然后,在WebConfiguration类中,重写addFormatters(FormatterRegistry registry)方法,并把BookFormatter类注册进去。

@RequestMapping(value = "/{isbn}/reviewers", method = RequestMethod.GET)
public List<Reviewer> getReviewers(@PathVariable("isbn") Book book) {
return book.getReviewers();
}

接下来,在BookController类中,新增一个请求方法,用来根据给定的图书的 isbn来显示评论者,

@RequestMapping(value = "/{isbn}/reviewers", method = RequestMethod.GET)
public List<Reviewer> getReviewers(@PathVariable("isbn") Book book) {
return book.getReviewers();
}

为了一些数据,现在手动添加一些测试数据填充数据库,通过向StartupRunner类添加两个自动装配的资源库:

@Autowired private AuthorRepository authorRepository;
@Autowired private PublisherRepository publisherRepository;

下面这些代码添加到StartupRunner类的run(...)方法中:

Author author = new Author("Alex", "Antonov");
author = authorRepository.save(author);
Publisher publisher = new Publisher("Packt");
publisher = publisherRepository.save(publisher);
Book book = new Book("978-1-78528-415-1", "Spring Boot Recipes", author, publisher);
bookRepository.save(book);

输入./gradlew clean bootRun,在控制台,启动应用程序。

访问http://localhost:8080/books/978-1-78528-415-1/reviewers,在浏览器中可以看到如下结果:

Spring Boot 学习(2)

格式化功能旨在提供与PropertyEditors类似的功能。 通过将FormatterRegistry注册在重写的addFormatter方法中,告诉Spring使用Formatter将Book的文本表示转换为实体对象并返回。 由于格式化是无状态的,因此我们无需在控制器中每次都要注册; 我们只做一次就好,这确保Spring为每个Web请求使用它。

Tips

如果要定义一个常用类型的转换(例如String或Boolean),就像我们在IsbnEditor示例中所做的那样,最好是通过Controller的InitBinder方法中的PropertyEditors初始化来做,因为这样的改变可能不是全局所期望的,只是针对特定的控制器的功能。

你可能已经注意到,我们还将BookRepository自动装配到WebConfiguration类,因为这是创建BookFormatter所需的。 这是Spring的一个很酷的东西,它让我们可以组合配置类,并使它们同时依赖于其他bean。 正如我们指出,为了创建一个WebConfiguration类,我们需要一个BookRepository,Spring确保在创建WebConfiguration类时首先创建BookRepository,然后自动注入作为依赖。 实例化WebConfiguration之后,将对其进行处理以进行配置说明。