前言 本章节所需的案例代码,可以直接从 GitHub 下载对应章节 spring-boot3-04
。
国际化 实现步骤 国际化的实现步骤如下:
1、Spring Boot 在类路径根下查找 messages
资源绑定文件,文件名默认为 messages.properties
2、多语言环境可以定义多个资源文件,命名规则为 messages_区域代码.properties
,如:messages.properties
:默认环境messages_zh_CN.properties
:中文环境messages_en_US.properties
:英文环境 3、在程序中可以自动注入 MessageSource
组件,获取国际化的配置项值 4、在页面中可以使用表达式 #{}
获取国际化的配置项值 常用配置 国际化的自动配置可参考 MessageSourceAutoConfiguration
自动配置类。
1 2 3 4 spring.messages.encoding =UTF-8 spring.messages.basename =messages
代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j @Controller public class LoginController { @Autowired public MessageSource messageSource; @GetMapping("/login") public String login (HttpServletRequest request) { Locale local = request.getLocale(); String login = messageSource.getMessage("login" , null , local); log.info("login: {}" , login); return "login" ; } }
路径匹配 Spring 5.3 之后加入了更多的请求路径匹配的实现策略,以前只支持 AntPathMatcher
策略,现在额外提供了 PathPatternParser
策略(默认),并且支持指定使用哪种策略。
Ant 风格 Ant 风格的路径模式语法具有以下规则:
*
:表示任意数量的字符。?
:表示任意一个字符。**
:表示任意数量的目录。{}
:表示一个命名的模式占位符。[]
:表示字符集合,例如 [a-z]
表示小写字母。Ant 风格的路径模式使用例子:
*.html
:匹配任意名称,扩展名为 .html
的文件/folder1/*/*.java
:匹配在 folder1
目录下的任意两级目录下的 .java
文件。/folder2/**/*.jsp
:匹配在 folder2
目录下任意目录深度的 .jsp
文件。/{type}/{id}.html
:匹配任意文件名为 {id}.html
,且在任意命名的 {type}
目录下的文件。特别注意
Ant 风格的路径模式语法中的特殊字符需要转义,如: 要匹配文件路径中的星号,则需要转义为 \\*
要匹配文件路径中的问号,则需要转义为 \\?
模式切换 1 2 3 4 5 @GetMapping("/a*/b?/{p1:[a-f]+}/**") public String hello (HttpServletRequest request, @PathVariable("p1") String path) { log.info("路径变量: {}" , path); return request.getRequestURI(); }
切换路径匹配策略,ant_path_matcher
是旧版策略,path_pattern_parser
是新版策略 1 2 spring.mvc.pathmatch.matching-strategy =ant_path_matcher
总结
SpringBoot 默认的路径匹配策略是由 PathPatternParser 提供的 如果路径中间需要有 **
,则需要切换为 Ant 风格的路径匹配策略 AntPathMatcher 内容协商 内容协商指的是一套系统适配多端数据的返回。
多端内容适配 默认规则 基于请求头内容协商(默认开启)客户端向服务端发送请求,携带 HTTP 标准的 Accept 请求头 客户端的请求头类型:Accept: application/json
、Accept: text/xml
、Accept: text/yaml
服务端根据客户端请求头期望的数据类型进行动态返回 基于请求参数内容协商(手动开启)发送请求 GET /projects/spring-boot?format=json
匹配到 @GetMapping("/projects/spring-boot")
根据请求参数协商,决定返回哪种数据类型,优先返回 JSON 类型数据 发送请求 GET /projects/spring-boot?format=xml
,优先返回 XML 类型数据 XML 内容协商案例 这里演示如何在请求同一个接口时,支持根据请求参数返回 JSON 或者 XML 格式的数据。
1 2 3 4 <dependency > <groupId > com.fasterxml.jackson.dataformat</groupId > <artifactId > jackson-dataformat-xml</artifactId > </dependency >
1 2 3 4 5 6 7 8 @JacksonXmlRootElement @Data public class Person { private Long id; private String userName; private String email; private Integer age; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import com.clay.boot.web.domain.Person;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller @RequestMapping("/person") public class PersonController { @ResponseBody @GetMapping("/get") public Person get () { Person person = new Person(); person.setId(1L ); person.setAge(18 ); person.setUserName("张三" ); person.setEmail("aaa@qq.com" ); return person; } }
1 2 3 4 5 spring.mvc.contentnegotiation.favor-parameter =true spring.mvc.contentnegotiation.parameter-name =type
提示
SpringBoot 默认支持接口返回 JSON 数据,因为 Web 场景启动器默认引入了 Jackson 处理 JSON 的包。 配置协商规则与支持类型 1 2 3 4 5 spring.mvc.contentnegotiation.favor-parameter =true spring.mvc.contentnegotiation.parameter-name =type
大多数 MediaType 都是开箱即用的,也可以自定义内容类型,如: 1 spring.mvc.contentnegotiation.media-types.yaml =text/yaml
自定义内容返回 YAML 内容协商案例 1 2 3 4 <dependency > <groupId > com.fasterxml.jackson.dataformat</groupId > <artifactId > jackson-dataformat-yaml</artifactId > </dependency >
新增一种媒体类型(MediaType),并开启基于请求参数的内容协商 1 2 3 4 5 6 7 8 spring.mvc.contentnegotiation.media-types.yaml =text/yaml spring.mvc.contentnegotiation.favor-parameter =true spring.mvc.contentnegotiation.parameter-name =type
编写可以支持 YAML 格式数据的 HttpMessageConverter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;import org.springframework.http.HttpInputMessage;import org.springframework.http.HttpOutputMessage;import org.springframework.http.MediaType;import org.springframework.http.converter.AbstractHttpMessageConverter;import org.springframework.http.converter.HttpMessageNotReadableException;import org.springframework.http.converter.HttpMessageNotWritableException;import java.io.IOException;import java.io.OutputStream;import java.nio.charset.StandardCharsets;public class YamlHttpMessageConverter extends AbstractHttpMessageConverter <Object > { private final ObjectMapper objectMapper; public YamlHttpMessageConverter () { super (new MediaType("text" , "yaml" , StandardCharsets.UTF_8)); YAMLFactory yamlFactory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER); this .objectMapper = new ObjectMapper(yamlFactory); } @Override protected boolean supports (Class<?> clazz) { return true ; } @Override protected Object readInternal (Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null ; } @Override protected void writeInternal (Object returnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { try (OutputStream outputStream = outputMessage.getBody()) { this .objectMapper.writeValue(outputStream, returnValue); } } }
添加 HttpMessageConverter
组件,专门负责把返回值对象输出为 YAML 格式的数据 1 2 3 4 5 6 7 8 9 10 @Configuration public class WebConfiguration implements WebMvcConfigurer { @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(new YamlHttpMessageConverter()); } }
自定义内容返回总结 如何自定义内容返回 1、配置媒体类型支持: spring.mvc.contentnegotiation.media-types.yaml=text/yaml
2、编写对应的 HttpMessageConverter
,在内部要告诉 SpringBoot 支持的媒体类型 3、往容器中放一个 WebMvcConfigurer
组件,并添加自定义的 HttpMessageConverter
内容协商原理分析 1、@ResponseBody
的底层由 HttpMessageConverter
处理数据,即标注了 @ResponseBody
的返回值,将会由支持它的 HttpMessageConverter
将数据返回给浏览器。
2、WebMvcAutoConfiguration
提供了 6 种 默认的 HttpMessageConverters
EnableWebMvcConfiguration
通过 addDefaultHttpMessageConverters
添加了默认的 MessageConverter
,如下:ByteArrayHttpMessageConverter
:支持字节数据读写StringHttpMessageConverter
:支持字符串读写ResourceHttpMessageConverter
:支持资源读写ResourceRegionHttpMessageConverter
:支持分区资源写出AllEncompassingFormHttpMessageConverter
:支持表单 XML/JSON 读写MappingJackson2HttpMessageConverter
:支持请求响应体 JSON 读写提示
SpringBoot 提供默认的 MessageConverter
功能有限,仅用于 JSON 或者普通的返回数据。如果需要增加新的内容协商功能,必须添加新的 HttpMessageConverter
。
模板引擎 模板引擎介绍 由于 SpringBoot 使用了嵌入式 Servlet 容器,所以 JSP 默认是不能使用的。如果需要服务端页面渲染,优先考虑使用模板引擎技术。
SpringBoot 默认包含了以下模板引擎的自动配置,模板引擎的页面默认放在 src/main/resources/templates
目录下。
Thymeleaf 整合 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 spring: thymeleaf: enabled: true cache: true mode: HTML suffix: .html encoding: UTF-8 prefix: classpath:/templates/ check-template-location: true
编写 Controller 类,往模板文件中存放值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.thymeleaf.util.StringUtils;@Controller public class WelcomeController { @GetMapping("/") public String welcome (@RequestParam(name = "name", required = false) String name, Model model) { if (StringUtils.isEmpty(name)) { name = "Thymeleaf" ; } model.addAttribute("name" , name); return "welcome" ; } }
编写 HTML 模板页面,显示在 Controller 类中设置的值 1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > Welcome</title > </head > <body > <h1 > Hello <span th:text ="${name}" > </span > </h1 > </body > </html >
浏览器访问 http://127.0.0.1:8080/?name=Peter
,显示的页面内容如下:
自动配置原理自动配置类是 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
配置属性绑定在 ThymeleafProperties
中,对应配置文件中以 spring.thymeleaf
开始前缀的内容 所有的模板页面默认都放在 classpath:/templates
文件夹下 自动配置实现的默认效果 所有的模板页面都在 classpath:/templates/
文件夹下面找 找后缀名为 .html
的模板页面进行渲染 Thymeleaf 基础语法 核心用法 th:xxx
:动态渲染指定的 HTML 标签属性值或者 th
指令(遍历、判断等)th:text
:渲染 HTML 标签体内的内容th:utext
:渲染 HTML 标签体内的内容,但不会转义,显示为 HTML 原本的样子th:任意HTML属性
:标签指定属性渲染th:attr
:标签任意属性渲染th:if
、th:each
:其他 th
指令1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > </head > <body > <p th:text ="${content}" > 原内容</p > <a th:href ="${url}" > 登录</a > <img th:src ="${imgUrl}" style ="width:300px; height: 200px" > <img th:attr ="src=${imgUrl},style=${imgStyle},title=#{logo},alt=#{logo}" > <img th:attr ="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" th:if ="${imgShow}" > </body > </html >
表达式:用来动态取值
${}
:变量取值,使用 Model
共享给页面的值都可以直接用 ${}
获取@{}
:URL 路径#{}
:国际化消息~{}
:片段引用*{}
:变量选择:需要配合 th:object
绑定对象系统工具 & 内置对象:官方文档
param
:请求参数对象session
:Session 对象application
:Application 对象#execInfo
:模板执行信息#messages
:国际化消息#uris
:URI/URL 工具#conversions
:类型转换工具#dates
:日期工具,是 java.util.Date
对象的工具类#calendars
:类似 #dates
,只不过是 java.util.Calendar
对象的工具类#temporals
: JDK 8+ 的 java.time API
工具类#numbers
:数字操作工具#strings
:字符串操作#objects
:对象操作#bools
:布尔操作#arrays
:Array 工具#lists
:List 工具#sets
:Set 工具#maps
:Map 工具#aggregates
:集合聚合工具(sum、avg)#ids
:ID 生成工具语法示例 表达式:
变量取值:${...}
URL 取值:@{...}
国际化消息:#{...}
变量选择:*{...}
片段引用: ~{...}
常见:
文本:one text
,another one!
,… 数字:0,34,3.0,12.3,… 布尔:true
、false
Null:null
变量名:one,sometext,main… 文本操作:
拼接字符串: +
文本内容替换:| The name is ${name} |
布尔操作:
比较运算:
比较:>
,<
,<=
,>=
(gt
,lt
,ge
,le
) 等值运算:==
,!=
(eq
,ne
) 条件运算:
if-then: (if)?(then)
if-then-else:(if)?(then):(else)
default:(value)?:(defaultValue)
特殊语法:
以上所有语法都可以嵌套组合
1 'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
集合遍历 语法:th:each="元素名, 迭代状态 : ${集合}"
迭代状态有以下属性:index
:当前遍历元素的索引,从 0 开始count
:当前遍历元素的索引,从 1 开始size
:需要遍历元素的总数量current
:当前正在遍历的元素对象even/odd
:是否偶数 / 奇数行first
:是否第一个元素last
:是否最后一个元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > Welcome</title > </head > <body > <table border ="1" cellspacing ="0" > <thead > <tr > <th > ID</th > <th > 姓名</th > <th > 年龄</th > <th > 邮箱</th > </tr > </thead > <tbody > <tr th:each ="person : ${persons}" > <td th:text ="${person.id}" > </td > <td th:text ="${person.userName}" > </td > <td th:text ="${person.age}" > </td > <td th:text ="${person.email}" > </td > </tr > </tbody > </table > <table border ="1" cellspacing ="0" > <thead > <tr > <th > ID</th > <th > 姓名</th > <th > 年龄</th > <th > 邮箱</th > </tr > </thead > <tbody > <tr th:each ="person, iterStat : ${persons}" th:class ="${iterStat.odd} ? 'odd'" th:index ="${iterStat.index}" > <td th:text ="${person.id}" > </td > <td th:text ="${person.userName}" > </td > <td th:text ="${person.age}" > </td > <td th:text ="${person.email}" > </td > </tr > </tbody > </table > </body > </html >
条件判断 1 2 3 <tr th:each ="person : ${persons}" > <td th:text ="${person.age >= 18 ? '成年人' : '未成年人'}" > </td > </tr >
1 <a href ="@{/comments.html}" th:if ="${not #lists.isEmpty(prod.comments)}" > view</a >
1 2 3 4 5 6 <div th:switch ="${person.role}" > <span th:case ="pm" > 项目经理</span > <span th:case ="admin" > 管理员</span > <span th:case ="hr" > HR</span > <span th:case ="*" > 其他</span > </div >
属性优先级 优先级(值越小优先级越高) 功能 属性 1 片段包含 th:insert
、th:replace
2 遍历 th:each
3 判断 th:if
、th:unless
、th:switch
、th:case
4 定义本地变量 th:object
、th:with
5 通用方式属性修改 th:attr
、th:attrprepend
、th:attrappend
6 指定属性修改 th:value
、th:href
、th:src
…7 文本值 th:text
、th:utext
8 片段指定 th:fragment
9 片段移除 th:remove
1 2 3 <ul > <li th:each ="item : ${items}" th:text ="${item.description}" > Item description here...</li > </ul >
行内写法 语法:[[...]] or [(...)]
1 <p > [[${session.user.name}]]</p >
等同于
1 <p th:text ="${session.user.name}" > </p >
变量选择 1 2 3 4 5 <div th:object ="${session.user}" > <p > Name: <span th:text ="*{firstName}" > Sebastian</span > .</p > <p > Surname: <span th:text ="*{lastName}" > Pepper</span > .</p > <p > Nationality: <span th:text ="*{nationality}" > Saturn</span > .</p > </div >
等同于
1 2 3 4 5 <div > <p > Name: <span th:text ="${session.user.firstName}" > Sebastian</span > .</p > <p > Surname: <span th:text ="${session.user.lastName}" > Pepper</span > .</p > <p > Nationality: <span th:text ="${session.user.nationality}" > Saturn</span > .</p > </div >
模板布局 模版布局的语法定义模板:th:fragment
引用模板:~{templateName::selector}
插入模板:th:insert
、th:replace
1 <span th:fragment ="copy" > © 2011 The Good Thymes Virtual Grocery</span >
引用模板布局(如在 index.html
中引用) 1 2 3 4 <body > <div th:insert ="~{footer :: copy}" > </div > <div th:replace ="~{footer :: copy}" > </div > </body >
实现的效果(如 index.html
最终的渲染结果) 1 2 3 4 5 6 <body > <div > <span > © 2011 The Good Thymes Virtual Grocery</span > </div > <span > © 2011 The Good Thymes Virtual Grocery</span > </body >
SpringBoot 提供了 Devtools
工具用于代码的热加载,首先引入依赖:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <optional > true</optional > </dependency >
更改 Thymeleaf 的模板页面后,使用快捷键 Ctrl + F9
就可以让页面的更改立即生效。值得一提的是,对于 Java 代码的修改,如果 Devtools
热启动了,可能会引起一些 Bug,且难以排查。
最新特性 Problemdetails Problemdetails
实现了 RFC 7807 规范,用于返回新格式的错误信息。
源码介绍 ProblemDetailsExceptionHandler
是一个 @ControllerAdvice
,用于集中处理系统异常1 2 3 4 5 6 7 8 9 10 11 @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true") static class ProblemDetailsErrorHandlingConfiguration { @Bean @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) ProblemDetailsExceptionHandler problemDetailsExceptionHandler () { return new ProblemDetailsExceptionHandler(); } }
1 2 3 4 @ControllerAdvice final class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {}
ProblemDetailsExceptionHandler
默认可以处理以下异常,如果系统出现以下异常,会被 SpringBoot 以 RFC 7807 规范的方式返回错误数据1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @ExceptionHandler({ HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, MissingServletRequestPartException.class, ServletRequestBindingException.class, MethodArgumentNotValidException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class, ErrorResponseException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, BindException.class })
使用说明 ProblemDetails 功能默认是关闭的,需要手动开启 1 spring.mvc.problemdetails.enabled =true
ProblemDetails 启用后,当 SpringBoot 捕获到异常后,默认会响应 JSON 数据和返回 HTTP 状态码 405
,且响应的 Header 是 Content-Type: application/problem+json
1 2 3 4 5 6 7 { "type" : "about:blank" , "title" : "Method Not Allowed" , "status" : 405 , "detail" : "Method 'POST' is not supported." , "instance" : "/list" }
函数式 Web Spring MVC 5.2
以后,允许使用函数式的方式定义 Web 的请求处理流程。
Web 请求处理的两种方式
1、@Controller
+ @RequestMapping
:耦合式(路由和业务耦合) 2、函数式 Web:分离式(路由和业务分离) 使用场景 场景:User RESTful - CRUDGET /user/1
:获取 1 号用户GET /users
:获取所有用户POST /user
:请求体携带 JSON,新增一个用户PUT /user/1
:请求体携带 JSON,修改 1 号用户DELETE /user/1
:删除 1 号用户 核心类 RouterFunction
:定义路由信息,即发送什么请求,由谁来处理RequestPredicate
:请求方式(如 GET、POST)、请求参数ServerRequest
:封装请求数据ServerResponse
:封装响应数据使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import lombok.ToString;@Data @ToString @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String userName; private String email; private Integer age; private String role; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import com.clay.boot.web.biz.UserBizHandler;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.MediaType;import org.springframework.web.servlet.function.RequestPredicate;import org.springframework.web.servlet.function.RequestPredicates;import org.springframework.web.servlet.function.RouterFunction;import org.springframework.web.servlet.function.RouterFunctions;import org.springframework.web.servlet.function.ServerResponse;@Configuration public class RoutingConfiguration { private static final RequestPredicate ACCEPT_ALL = RequestPredicates.accept(MediaType.ALL); private static final RequestPredicate ACCEPT_JSON = RequestPredicates.accept(MediaType.APPLICATION_JSON); @Bean public RouterFunction<ServerResponse> userRoute (UserBizHandler userBizHandler) { return RouterFunctions.route() .GET("/user/{id}" , ACCEPT_ALL, userBizHandler::getUser) .GET("/users" , ACCEPT_ALL, userBizHandler::listUser) .POST("/user" , ACCEPT_JSON, userBizHandler::addUser) .PUT("/user/{id}" , ACCEPT_JSON, userBizHandler::updateUser) .DELETE("/user/{id}" , ACCEPT_ALL, userBizHandler::deleteUser) .build(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import com.clay.boot.web.domain.User;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springframework.web.servlet.function.ServerRequest;import org.springframework.web.servlet.function.ServerResponse;import java.util.ArrayList;import java.util.List;@Slf4j @Component public class UserBizHandler { public ServerResponse getUser (ServerRequest serverRequest) throws Exception { Long id = Long.parseLong(serverRequest.pathVariable("id" )); User user = new User(id, "Peter1" , "peter@gmail.com" , 18 , "pm" ); return ServerResponse.ok().body(user); } public ServerResponse listUser (ServerRequest serverRequest) throws Exception { List<User> users = new ArrayList<>(); users.add(new User(1L , "Peter1" , "peter@gmail.com" , 18 , "pm" )); users.add(new User(2L , "Peter2" , "peter@gmail.com" , 16 , "admin" )); users.add(new User(3L , "Peter3" , "peter@gmail.com" , 18 , "pm" )); return ServerResponse.ok().body(users); } public ServerResponse addUser (ServerRequest serverRequest) throws Exception { User user = serverRequest.body(User.class); log.info("user save success, {}" , user.toString()); return ServerResponse.ok().build(); } public ServerResponse deleteUser (ServerRequest serverRequest) throws Exception { Long id = Long.parseLong(serverRequest.pathVariable("id" )); log.info("user {} delete success" , id); return ServerResponse.ok().build(); } public ServerResponse updateUser (ServerRequest serverRequest) throws Exception { User user = serverRequest.body(User.class); Long id = Long.parseLong(serverRequest.pathVariable("id" )); log.info("user {} update success, {}" , id, user.toString()); return ServerResponse.ok().build(); } }