SpringBoot从入门到精通系列四:RESTful服务支持
- 一、开发RESTful服务
- 1.基于JSON的RESTful服务
- 2.创建pom.xml
- 3.控制器类
- 4.Dao层Book类
- 5.Service层BookService
- 6.主程序
- 7.准备book.json
- 二、SpringBoot内置的JSON支持
- 1.Gson支持
- 2.创建pom.xml
- 3.创建application.properties设置json属性
- 4.dao层Book类
- 5.service层BookService
- 6.controller类
- 7.主类
- 三、自定义JSON序列化器和反序列化器
- 四、使用HttpMessageConverters更换转换器
- 1.创建pom.xml
- 2.创建配置类注册自定义的HttpMessageConverter
- 五、跨域资源共享
- 1.设置全局的CORS配置
- 2.创建pom.xml
- 3.定义控制器类BookController
- 4.前端模拟前后端分离架构
- 六、RESTful客户端
- 1.使用RestTemplate调用RESTful服务
- 2.修改主程序
- 3.Dao层book类
- 4.Service服务ClientService
开发RESTful服务无须提供视图页面,直接使用JSON数据或XML数据作为响应,这种JSON响应或XML响应将会交给前端应用解析、呈现。
一、开发RESTful服务
RESTful服务是前后端分离架构中的主要功能:
- 后端应用对外暴露RESTful服务
- 前端应用则通过RESTful服务与后端应用交互
- RESTful服务的数据格式既是JSON的,也是XML的
1.基于JSON的RESTful服务
开发基于JSON的RESTful服务非常简单,只要使用@RestController注解修饰控制器类或者使用@ResponseBody修饰处理方法即可。
- RestController和@Controller的区别在于,@RestController会自动为每个处理方法都添加@ResponseBody注解
2.创建pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<groupId>org.crazyit</groupId>
<artifactId>Restful</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Restful</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 定义Spring Boot Maven插件,可用于运行Spring Boot应用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.控制器类
下面的控制器类示范了如何开发基于JSON的RESTful服务
\controller\BookController.java
import org.crazyit.app.domain.Book;
import org.crazyit.app.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@RestController
@RequestMapping("/book")
public class BookController
{
@Autowired
private BookService bookService;
@PostMapping("")
public Book create(@RequestBody Book book)
{
return this.bookService.createOrUpdate(book);
}
@PutMapping("")
public Book update(@RequestBody Book book)
{
Objects.requireNonNull(book);
return this.bookService.createOrUpdate(book);
}
@GetMapping("")
public Collection<Book> list()
{
return this.bookService.list();
}
}
- 该控制器类使用了@RestController注解修饰,相当于为所有处理方法都添加了@ResponseBody修饰,这样SpringBoot将会直接用这些处理方法的返回值作为响应
- 只是用@Controller注解修饰该控制器类,则还需要为处理方法额外添加@ResponseBody注解进行修饰
4.Dao层Book类
public class Book
{
private Integer id;
private String title;
private double price;
private String author;
public Book(){}
public Book(Integer id, String title, double price, String author)
{
this.id = id;
this.title = title;
this.price = price;
this.author = author;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
}
5.Service层BookService
\app\service\BookService.java
import java.util.Collection;
import org.crazyit.app.domain.Book;
public interface BookService
{
Collection<Book> list();
Book createOrUpdate(Book book);
}
\app\service\impl\BookServiceImpl.java
import org.crazyit.app.domain.Book;
import org.crazyit.app.service.BookService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class BookServiceImpl implements BookService
{
private final Map<Integer, Book> data = new ConcurrentHashMap<>();
private static final AtomicInteger idGenerator = new AtomicInteger(0);
@Override
public Collection<Book> list()
{
return this.data.values();
}
@Override
public Book createOrUpdate(Book book)
{
// 修改图书
if (book.getId() != null && data.containsKey(book.getId()))
{
this.data.put(book.getId(), book);
}
else
{
Integer id = idGenerator.incrementAndGet();
book.setId(id);
this.data.put(id, book);
}
return book;
}
}
6.主程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App
{
public static void main( String[] args )
{
SpringApplication.run(App.class, args);
}
}
7.准备book.json
{
"title": "java",
"price": 180,
"author": "努力学java"
}
然后输入命令:
curl -H "Content-Type: application/json" -X POST -d @book.json http://localhost:8080/book
上面命令向http://localhost:8080/book提交POST请求,以book.json文件的内容作为请求参数,运行上面命令,将会看到如下输出:
curl -H "Content-Type: application/json" -X POST -d @book.json http://localhost:8080/book
{“id":1,"title":"java","price":180,"author":"努力学java"}
通过上面输出可以看出,上面提交的POST请求向后端应用添加了一个Book对象。
再输入如下命令:
curl http://localhost:8080/book
上面命令没有指定任何特殊选项,默认向http://localhost:8080/book发送GET请求。
运行上面命令,将会看到如下输出:
curl http://localhost:8080/book
[{“id":1,"title":"java","price":180,"author":"努力学java"}]
通过上面输出可以看出,上面提交的GET请求可获取后端应用中所有的Book对象。
可以通过多多次提交POST请求来添加多个Book对象,然后通过GET请求即可看到后端应用所包含的全部Book对象。
二、SpringBoot内置的JSON支持
SpringBoot内置了如下三种JSON库的支持:
- Jackson
- Gson
- JSON-B
如果没有任何特别的配置,SpringBoot默认选择Jackson作为JSON库。Jackson的自动配置由spring-boot-starter-json.jar提供,只要SpringBoot检测到系统类加载路径中有Jackson依赖库,SpringBoot就会自动创建基于Jackson的ObjectMapper。
1.Gson支持
如果希望使用Gson作为JSON解析库,只要从依赖配置中排除spring-boot-starter-json,并添加Gson依赖库即可。
只要SpringBoot检测到类加载路径中包含了Gson库,SpringBoot就会自动配置一个Gson Bean,该Bean负责为Gson提供自动配置支持。
SpringBoot为Gson提供了如下常用的配置属性:
- spring.gson.pretty-printing:指定是否对JSON字符串执行格式化
- spring.gson.date-format:指定日期的序列化格式,比如yyyy-MM-dd
- spring.gson.serialize-nulls:指定是否序列化null值
- spring.gson.disable-html-escaping:指定是否禁用HTML转义
- spring.gson.disable-inner-class-serialization:指定是否禁用内部类的序列化
- spring.gson.enable-complex-map-key-serialization:指定是否对复合的Map key启用序列化
通过spring.gson.*这些属性还不能完全定制Gson的JSON序列化行为,SpringBoot也允许在容器中配置一个或多个GsonBuilderCustomizer,然后实现customize(GsonBuilder gsonBuilder)方法来定制序列化行为
2.创建pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<groupId>org.crazyit</groupId>
<artifactId>Restful_Gson</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Restful_Gson</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除spring-boot-starter-json(默认使用Jackson)-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加Gson库 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 定义Spring Boot Maven插件,可用于运行Spring Boot应用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 配置中排除了spring-boot-starter-json,就排除了默认的Jackson库。
- 添加了Gon,SpringBoot就会在类加载路径中检测到Gson库,这样SpringBoot就会启用Gson作为JSON支持
3.创建application.properties设置json属性
\Restful_Gson\src\main\resources\application.properties
# 格式化JSON字符串
spring.gson.pretty-printing=true
# 指定日期格式
spring.gson.date-format=yyyy-MM-dd
# 指定序列化null值
spring.gson.serialize-nulls=true
# 指定不禁用HTML转义
spring.gson.disable-html-escaping=false
# 指定不禁用内部类的序列化
spring.gson.disable-inner-class-serialization=false
# 指定启用复合Map key的序列化
spring.gson.enable-complex-map-key-serialization=true
4.dao层Book类
public class Book
{
private Integer id;
private String title;
private double price;
private String author;
public Book(){}
public Book(Integer id, String title, double price, String author)
{
this.id = id;
this.title = title;
this.price = price;
this.author = author;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
}
5.service层BookService
BookService:
import org.crazyit.app.domain.Book;
import java.util.Collection;
public interface BookService
{
Collection<Book> list();
Book createOrUpdate(Book book);
}
BookServiceImpl
import org.crazyit.app.domain.Book;
import org.crazyit.app.service.BookService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class BookServiceImpl implements BookService
{
private final Map<Integer, Book> data = new ConcurrentHashMap<>();
private static final AtomicInteger idGenerator = new AtomicInteger(0);
@Override
public Collection<Book> list()
{
return this.data.values();
}
@Override
public Book createOrUpdate(Book book)
{
// 修改图书
if (book.getId() != null && data.containsKey(book.getId()))
{
this.data.put(book.getId(), book);
}
else
{
Integer id = idGenerator.incrementAndGet();
book.setId(id);
this.data.put(id, book);
}
return book;
}
}
6.controller类
import org.crazyit.app.domain.Book;
import org.crazyit.app.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@RestController
@RequestMapping("/book")
public class BookController
{
@Autowired
private BookService bookService;
@PostMapping("")
public Book create(@RequestBody Book book)
{
return this.bookService.createOrUpdate(book);
}
@PutMapping("")
public Book update(@RequestBody Book book)
{
Objects.requireNonNull(book);
return this.bookService.createOrUpdate(book);
}
@GetMapping("")
public Collection<Book> list()
{
return this.bookService.list();
}
}
7.主类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import java.util.Arrays;
@SpringBootApplication
public class App
{
public static void main( String[] args )
{
ApplicationContext ctx = SpringApplication.run(App.class, args);
System.out.println(Arrays.toString(ctx.getBeanDefinitionNames()));
}
}
运行主类来启动应用,然后运行curl命令提交POST请求添加图书,则可看到如下输出:
curl -H "Content-Type: application/json" -X POST -d @book.json http://localhost:8080/book
{
"id": 1,
"title":"java",
"price":180,
"author":"努力学java"
}
可以看到,此时服务器端生成的响应是格式化后的JSON字符串,这就是将spring.gson.pretty-printing属性指定为true的效果。
当实际项目上线时,一般不推荐服务器端生成格式良好的JSON响应,因为格式良好的JSON响应需要添加额外的空白、换行符等字符,这样会增加额外的性能负担。
三、自定义JSON序列化器和反序列化器
注册自定义的序列化器和反序列化器有两种方式:
- 利用Jackson的模块机制来注册自定义的序列化器和反序列化器
- 利于SpringBoot提供的@JsonComponent注解来注册自定义的序列化器和反序列化器,该注解有两种使用方式:
直接使用一个外部类来包含所有序列化器和反序列化器显得内聚性更好,本例使用的控制器类、Service组件、Book类和上面一致。
\Restful_Custom\src\main\java\org\crazyit\app\controller\BookSerialize.java
package org.crazyit.app.controller;
import java.io.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import org.crazyit.app.domain.Book;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.jackson.*;
@JsonComponent
public class BookSerialize
{
public static class Serializer extends JsonSerializer<Book>
{
@Override
public void serialize(Book book, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException
{
System.out.println("序列化");
// 输出对象开始的Token(也就是左花括号)
jsonGenerator.writeStartObject();
// 依次输出Book的4个属性
jsonGenerator.writeNumberField("id", book.getId());
// 对book的title属性,此处序列化为name
jsonGenerator.writeObjectField("name", book.getTitle());
jsonGenerator.writeObjectField("author", book.getAuthor());
jsonGenerator.writeNumberField("price", book.getPrice());
jsonGenerator.writeEndObject();
}
}
public static class Deserializer extends JsonDeserializer<Book>
{
@Override
public Book deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException
{
System.out.println("反序列化");
var book = new Book();
// 开始解析JSON字符串
JsonToken jsonToken = jsonParser.getCurrentToken();
String fieldName = null;
// 如果还未解析到对象结束
while (!jsonToken.equals(JsonToken.END_OBJECT))
{
if (!jsonToken.equals(JsonToken.FIELD_NAME))
{
jsonToken = jsonParser.nextToken();
continue;
}
// 解析到field名
fieldName = jsonParser.getCurrentName();
// 解析解析下一个token(field名之后就是field值)
jsonToken = jsonParser.nextToken();
try
{
// 如果fieldName是name,则为field值的前后添加书名号
if (fieldName.equals("name"))
{
String name = jsonParser.getText();
if (!name.startsWith("《"))
{
name = "《" + name;
}
if (!name.endsWith("》"))
{
name = name + "》";
}
book.setTitle(name);
}
// 如果fieldName是price,则将价格打8折
else if (fieldName.equals("price"))
{
book.setPrice(jsonParser.getDoubleValue() * 0.8);
}
// 对于其他fieldName,调用fieldName默认对应的setter方法
else
{
BeanUtils.getPropertyDescriptor(Book.class, fieldName)
.getWriteMethod().invoke(book, jsonParser.getText());
}
// 解析解析下一个Token
jsonToken = jsonParser.nextToken();
} catch (Exception e)
{
System.out.println("反序列化过程中出现异常:" + e);
}
}
return book;
}
}
}
- 上面外部类使用了@JsonComponent修饰,且该外部类包含了两个内部类,分别实现了JsonSerializer<Book>和JsonDeserializer<Book>,因此这两个内部类将会作为Jackson自定义的JSON序列化器和反序列化器。
- 上面内部类中实现JsonSerializer接口的是序列化器,序列化器必须实现serialize()方法,该方法负责将Java对象序列化成JSON字符串。该方法中的JsonGenerator参数就负责完成输出,该参数提供了如下常用方法:
- 上面内部类中实现JsonDeserializer接口的是反序列化器,反序列化器必须实现deserialize()方法,该方法负责将JSON字符串恢复成Java对象。该方法中的JsonParser负责读取JSON字符串所包含的数据,并将其封装成Java对象
- JsonParser采用不断获取下一个Token的方式来读取JSON字符串所包含的数据,比如数组开始、数组结束、对象开始、对象结束、属性等各种Token,Jackson使用JsonToken枚举来定义所有类型的Token
准备book.json文件:
{
”name“: "Java讲义",
"price":128,
”author“: "努力学java"
}
在命令行窗口中使用curl命令提交POST请求,可以看到如下输出:
curl -H "Content-Type: application/json" -X POST -d @book.json http://localhost:8080/book
{"id":1,"name":"<<Java讲义>>","author":"努力学java","price":102.4}
在命令行窗口中使用curl命令提交GET请求,可以看到如下输出:
curl http://localhost:8080/book
[{"id":1,"name":"<<Java讲义>>","author":"努力学java","price":102.4}]
四、使用HttpMessageConverters更换转换器
@RequestBody、@ResponseBody两个注解的功能是基于HttpMessageConverter提供了对应的实现类,能把请求体中的数据转换成Java对象或HttpEntity,也能将处理方法返回的Java对象或ResponseEntity转换成响应。
SpringBoot提供了HttpMessageConverters来添加自定义的HttpMessageConverter,这样即可改为使用第三方的JSON或XML解析库,不再依赖spring-boot-starter-parent,并添加spring-boot-starter-web.jar依赖。
1.创建pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<groupId>org.crazyit</groupId>
<artifactId>Restful_Fastjson</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Restful_Fastjson</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除spring-boot-starter-json(默认使用Jackson)-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加Fastjson库 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 定义Spring Boot Maven插件,可用于运行Spring Boot应用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 添加了Fastjson依赖,这样即可使用该JAR包所提供的HttpMessageConverter实现类。
2.创建配置类注册自定义的HttpMessageConverter
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.*;
import org.springframework.http.MediaType;
import org.springframework.http.converter.*;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import java.util.ArrayList;
import java.util.List;
@Configuration(proxyBeanMethods = false)
public class FkConfiguration
{
@Bean
public HttpMessageConverters customConverters()
{
// 创建自定义的HttpMessageConverter
var fastJson = new FastJsonHttpMessageConverter();
// 设置FastJsonHttpMessageConverter支持的各种MediaType
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
supportedMediaTypes.add(MediaType.APPLICATION_PDF);
supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XML);
supportedMediaTypes.add(MediaType.IMAGE_GIF);
supportedMediaTypes.add(MediaType.IMAGE_JPEG);
supportedMediaTypes.add(MediaType.IMAGE_PNG);
supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
supportedMediaTypes.add(MediaType.TEXT_HTML);
supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
supportedMediaTypes.add(MediaType.TEXT_XML);
fastJson.setSupportedMediaTypes(supportedMediaTypes);
// 创建配置对象
var config = new FastJsonConfig();
// 为FastJsonConfig设置各种特性,准备供FastJsonHttpMessageConverter使用
config.setSerializerFeatures(
// 禁用循环检测
SerializerFeature.DisableCircularReferenceDetect,
// 输出Map的空value值
SerializerFeature.WriteMapNullValue,
// 输出格式良好的JSON字符串
SerializerFeature.PrettyFormat
);
fastJson.setFastJsonConfig(config);
// 通过HttpMessageConverters设置使用自定义的HttpMessageConverter
return new HttpMessageConverters(fastJson);
}
}
- 上面的配置类使用@Bean注解定义了一个Bean,它是一个HttpMessageConverters对象
- 该对象组合了第三方的HttpMessageConverter实现类对象:FastJsonHttpMessageConverter,使用第三方的FastJsonHttpMessageConverter作为HttpMessageConverter实现类
五、跨域资源共享
在前后端分离的开发架构中,前端应用和后端应用往往是彻底隔离的,二者不在同一台应用服务器内,甚至不在同一个物理节点上。
这种架构下,前端应用可能采用前端框架Vue等向后端应用发送请求,这种请求就是跨域请求,后端应用需要允许跨域资源共享(CORS)
- SpringBoot的跨域资源共享依然直接使用Spring MVC的@CrossOrigin注解,只要使用@CrossOrigin注解修饰控制器的处理方法即可
- SpringBoot会通过在容器中定义一个WebMvcConfigurer Bean,并在该Bean中实现自定义的addCorsMappings(CorsRegistry)方法来设置全局的CORS配置
1.设置全局的CORS配置
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer
{
@Override
public void addCrosMappings(CorsRegistry registry)
{
//指定对于/api/**路径下的所有请求
registry.addMapping("/api/**")
//允许接收来自http://www.a.org和http://www.b.org的请求
.allowedOrigins("http://www.a.org","http://www.b.org")
//允许处理GET、PUT、POST、Delete、PATCH请求
.allowedMethods("GET","PUT","POST","DELETE","PATCH")
//只允许哪些请求头
.allowedHeaders("header1","header2","header3")
.allowCredentials(true).maxAge(3600);
//指定对于/api/路径下的所有请求
//指定对于/root/**路径下的所有请求
registry.addMapping("/root/**")
//允许接收来自http://www.a.org的请求
.allowedOrigins("http://www.a.org")
//允许处理GET、POST请求
.allowedMethods("GET","POST")
.allowCredentials(true).maxAge(1800);
//下面还可针对其他路径添加配置
// ...
}
}
- 使用CorsRegistry调用了两次addMapping()方法,第一次调用addMapping()方法的规则对/api/路径及其所有子路径有效,指定只允许来自http://www.a.org和http://www.b.org的跨域访问,而且可接受GET、PUT、POST、DELETE和PATCH请求,最大缓存实际是1小时
2.创建pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<groupId>org.crazyit</groupId>
<artifactId>CrossOrigin</artifactId>
<version>1.0-SNAPSHOT</version>
<name>CrossOrigin</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 定义Spring Boot Maven插件,可用于运行Spring Boot应用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.定义控制器类BookController
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/book")
public class BookController
{
// 添加注解,指定支持跨域资源共享
@CrossOrigin(maxAge = 3600)
@GetMapping("")
public ResponseEntity<List<String>> books()
{
var books = List.of("疯狂Java讲义",
"疯狂Python讲义",
"轻量级Java Web企业应用实战",
"疯狂Android讲义");
return new ResponseEntity<>(books, HttpStatus.OK);
}
}
- 上面控制器类的处理方法使用了@CrossOrigin注解修饰,该方法就可以处理跨域请求了。
4.前端模拟前后端分离架构
<!DOCTYPE html>
<html>
<head>
<meta name="author" content="Yeeku.H.Lee(CrazyIt.org)" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="resources/bootstrap-4.3.1/css/bootstrap.min.css">
<script src="resources/jquery-3.4.1.min.js">
</script>
<script src="resources/bootstrap-4.3.1/js/bootstrap.min.js">
</script>
<title> 查看图书 </title>
</head>
<body>
<div class="container">
<a id="bn" href="#" class="btn btn-primary">查看作者的图书</a>
<div class="toast" role="alert" id="resultToast" data-delay="900000">
<div class="toast-header">
<h5>作者的图书</h5>
<button type="button" class="ml-2 mb-1 close"
data-dismiss="toast" aria-label="关闭">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="toast-body" id="content">
</div>
</div>
<div>
<script type="text/javascript">
$('#bn').click(function(){
$.get("http://192.168.1.88:8080/book", null, function(data){
// 清空content元素里的内容
$("#content").html("");
$("#content").append("<ul class='list-group'>");
// 遍历data数组,为每个数组元素添加一个li元素
for (b in data)
{
$("#content").append("<li class='list-group-item'>"
+ data[b] + "</li>");
}
$("#content").append("</ul>");
$('#resultToast').toast('show');
})
});
</script>
</body>
</html>
前端应用需要向http://192.168.1.88:8080/book发送请求,地址就是SpringBoot应用中BookController提供RESTful服务的地址
首先运行SpringBoot应用的主类来启动该应用(必须在IP地址为192.168.1.88的计算机上运行该应用),然后再部署、运行test前端应用。
六、RESTful客户端
SpringBoot不仅可以对外暴露RESTful服务,而且可以作为客户端调用远程RESTful服务。
SpringBoot主要提供了两种方式来调用远程RESTful服务:
- 使用RestTemplate调用RESTful服务
- 使用WebClient调用RESTful服务
1.使用RestTemplate调用RESTful服务
RestTemplate是Spring本身提供的API。SpringBoot提供的是RestTemplateBuilder,是RestTemplate的构建器,可对构建的RestTemplate应用SpringBoot的当前配置,也可调用方法对它构建的RestTemplate进行定制。
- 通过RestTemplateBuilder构建RestTemplate之后,接下来即可调用RestTemplate的方法来调用远程RESTful服务
RestTemplate提供了如下几类方法:
- delete(String url,Map<String,?> uriVariables):以DELETE请求调用远程RESTful服务。最后一个参数用于为URL地址中的路径参数(PathVariable)指定参数值。
- getForEntity(String url,ClassresponseType,Map<String,?> uriVariables):以GET请求调用远程RESTful服务。最后一个参数用于为URL地址中的路径参数(PathVariable)指定参数值。该方法的返回值直接是T类型的对象。
- headForHeaders(String url,Map<String,?> uriVariables):以HEAD请求调用远程RESTful服务。最后一个参数用于为URL地址中的路径参数(PathVariable)指定参数值。该方法返回服务器端响应的所有请求头。
- patchForObject(String url,Object request,Class responseType,Map<String,?> uriVariables):以PATCH请求调用远程RESTful服务,其中request参数代表请求参数。最后一个参数用于为URL地址中的路径参数(PathVariable)指定参数值。该方法的返回值直接是T类型的对象。
- postForEntity(String url,Object request,Class responseType,Map<String,?> uriVariables):该方法返回服务器端响应的ResponseEntity
- postForObject(String url,Object request,Class responseType,Map<String,?> uriVariables):该方法的返回值直接是T类型的对象
- put(String url,Object request, Map<String,?> uriVariables):以put请求调用远程RESTful服务,其中request参数代表请求参数。
上面这些方法分别对应向RESTful服务器端发送DELETE、GET、HEAD、PATCH、POST、PUT方式的请求
RestTemplate还提供了如下通用方法:
- execute(String url,HttpMethod method,RequestCallback requestCallback,ResponseExtractor responseExtractor,Map<String,?> uriVariables):该方法以method方式的请求调用远程RESTful服务,其中requestCallback参数用于准备要发送的请求,responseExtractor参数用于提取服务器端响应的数据。
- exchange(String url,HttpMethod method,HttpEntity<?> requestEntity,Class responseType,Map<String,?> uriVariables):该方法以method方式的请求调用远程RESTful服务,其中requestEntity参数用于指定请求参数。
exchange()是一个功能强大的通用方法,可以发送DELETE、GET、HEAD、PATCH、POST、PUT方式的请求
2.修改主程序
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
var application = new SpringApplication(App.class);
// 设置不再启动Web应用
application.setWebApplicationType(WebApplicationType.NONE);
// 或显式设置Spring Boot应用所使用的Spring容器
// application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
application.run(args);
}
}
将SpringBoot应用改为非Web应用两种方式:
- 调用application的setWebApplicationType(WebApplicationType.NONE)
- 调用application的setApplicationContextClass()方法显示设置Spring容器的实现类
3.Dao层book类
public class Book
{
private Integer id;
private String title;
private double price;
private String author;
public Book(){}
public Book(String title, double price, String author)
{
this.title = title;
this.price = price;
this.author = author;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
@Override
public String toString()
{
return "Book{" +
"id=" + id +
", title='" + title + '\'' +
", price=" + price +
", author='" + author + '\'' +
'}';
}
}
4.Service服务ClientService
SpringBoot提供了两个特殊接口:
- CommandLineRunner和ApplicationRunner
- 两个接口功能是一样的,SpringBoot会在SpringApplication启动完成之前自动调用CommandLineRunner或ApplicationRunner实现类的run()方法
- 通过这两个接口可以非常方便地在SpringApplication启动后运行一段代码
import org.crazyit.app.domain.Book;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Service
public class ClientService implements CommandLineRunner
{
@Override
public void run(String... args)
{
// System.out.println(this.callCreate());
// System.out.println(this.callList());
// this.callUpdate();
// System.out.println(this.callList());
// System.out.println(this.callExchange());
System.out.println(this.callExecute());
}
private final RestTemplate restTemplate;
public ClientService(RestTemplateBuilder restTemplateBuilder)
{
// 设置它请求URL的根路径
this.restTemplate = restTemplateBuilder.rootUri("http://192.168.1.188:8080/")
.build(); // ①
}
public Book callCreate()
{
var book = new Book("疯狂Java讲义", 139.0, "李");
return this.restTemplate.postForObject("/book", book, Book.class);
}
public void callUpdate()
{
var book = new Book("疯狂Android讲义", 129.0, "李");
book.setId(1);
this.restTemplate.put("/book", book);
}
@SuppressWarnings("unchecked")
public List<Book> callList()
{
return this.restTemplate.getForObject("/book", List.class);
}
public Book callExchange()
{
var book = new Book("疯狂Python讲义", 128.0, "李");
book.setId(1);
// 创建HttpEntity作为请求参数
var requestEntity = new HttpEntity<>(book);
ResponseEntity<Book> resEntity = this.restTemplate
.exchange("/book", HttpMethod.PUT, requestEntity, Book.class);
System.out.println("服务器响应码:" + resEntity.getStatusCodeValue());
return resEntity.getBody();
}
public String callExecute()
{
return this.restTemplate
.execute("/book", HttpMethod.PUT, request -> {
// 设置Accept请求头
request.getHeaders().setAccept(List.of(MediaType.APPLICATION_JSON));
// 设置Content-Type请求头
request.getHeaders().set("Content-Type", "application/json");
// 定义请求体的数据
byte[] json = ("{\"id\":1, \"title\": \"疯狂Android讲义\", " +
"\"price\": 129.0, \"author\":\"李\"}").getBytes(StandardCharsets.UTF_8);
// 设置请求体
request.getBody().write(json);
}, response -> {
System.out.println("code:" + response.getStatusCode());
System.out.println("text:" + response.getStatusText());
InputStream is = response.getBody();
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
});
}
}
- 上面的ClientService实现了CommandLineRunner接口,因此SpringBoot会在SpringApplication启动后自动执行该组件的run()方法
- 该run()方法仅仅是调用了callCreate()方法,该方法中的粗体字代码调用RestTemple的postForObject()方法发送POST请求调用RESTful服务
- ClientService定义了一个带RestTemplateBuilder参数的构造器,SpringBoot将会自动为该构造器注入RestTemplateBuilder参数,该构造器中this.restTemplate.postForObject("/book", book, Book.class)先为RestTemplate设置了root URL,然后调用build()方法构建RestTemplate
- ClientService还用到一个Book类,是一个DTO(Data Transfer Object)对象
首先运行RESTful服务器端的主程序,运行本例程序,将会在控制台看到如下输出:
Book{id = 1,titile='java',price=180,author='努力学java'}
将Clientservice类中的run()方法改为调用callList()方法,再次运行本例的主程序,会看到如下输出:
[{id = 1,titile='java',price=180,author='努力学java'}]