SpringBootWeb开发-2-3

上一级页面:ssm-spring-boot速成学习索引

前言

33、请求处理-【源码分析】-Servlet API参数解析原理

  • WebRequest
  • ServletRequest
  • MultipartRequest
  • HttpSession
  • javax.servlet.http.PushBuilder
  • Principal
  • InputStream
  • Reader
  • HttpMethod
  • Locale
  • TimeZone
  • ZoneId

通过上一节的调试方法得到:

ServletRequestMethodArgumentResolver用来处理以上的参数

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

@Nullable
private static Class<?> pushBuilder;

static {
try {
pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
ServletRequestMethodArgumentResolver.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Servlet 4.0 PushBuilder not found - not supported for injection
pushBuilder = null;
}
}


@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}

@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

Class<?> paramType = parameter.getParameterType();

// WebRequest / NativeWebRequest / ServletWebRequest
if (WebRequest.class.isAssignableFrom(paramType)) {
if (!paramType.isInstance(webRequest)) {
throw new IllegalStateException(
"Current request is not of type [" + paramType.getName() + "]: " + webRequest);
}
return webRequest;
}

// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
return resolveNativeRequest(webRequest, paramType);
}

// HttpServletRequest required for all further argument types
return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
}

private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
T nativeRequest = webRequest.getNativeRequest(requiredType);
if (nativeRequest == null) {
throw new IllegalStateException(
"Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
}
return nativeRequest;
}

@Nullable
private Object resolveArgument(Class<?> paramType, HttpServletRequest request) throws IOException {
if (HttpSession.class.isAssignableFrom(paramType)) {
HttpSession session = request.getSession();
if (session != null && !paramType.isInstance(session)) {
throw new IllegalStateException(
"Current session is not of type [" + paramType.getName() + "]: " + session);
}
return session;
}
else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) {
return PushBuilderDelegate.resolvePushBuilder(request, paramType);
}
else if (InputStream.class.isAssignableFrom(paramType)) {
InputStream inputStream = request.getInputStream();
if (inputStream != null && !paramType.isInstance(inputStream)) {
throw new IllegalStateException(
"Request input stream is not of type [" + paramType.getName() + "]: " + inputStream);
}
return inputStream;
}
else if (Reader.class.isAssignableFrom(paramType)) {
Reader reader = request.getReader();
if (reader != null && !paramType.isInstance(reader)) {
throw new IllegalStateException(
"Request body reader is not of type [" + paramType.getName() + "]: " + reader);
}
return reader;
}
else if (Principal.class.isAssignableFrom(paramType)) {
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
throw new IllegalStateException(
"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
}
return userPrincipal;
}
else if (HttpMethod.class == paramType) {
return HttpMethod.resolve(request.getMethod());
}
else if (Locale.class == paramType) {
return RequestContextUtils.getLocale(request);
}
else if (TimeZone.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone : TimeZone.getDefault());
}
else if (ZoneId.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
}

// Should never happen...
throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
}


/**
* Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
*/
private static class PushBuilderDelegate {

@Nullable
public static Object resolvePushBuilder(HttpServletRequest request, Class<?> paramType) {
PushBuilder pushBuilder = request.newPushBuilder();
if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
throw new IllegalStateException(
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
}
return pushBuilder;

}
}
}

用例:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class RequestController {

@GetMapping("/goto")
public String goToPage(HttpServletRequest request){

request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //转发到 /success请求
}
}

34、请求处理-【源码分析】-Model、Map原理

复杂参数:

  • Map

  • Model(map、model里面的数据会被放在request的请求域 request.setAttribute)

  • Errors/BindingResult

  • RedirectAttributes( 重定向携带数据)

  • ServletResponse(响应response)

  • SessionStatus

  • UriComponentsBuilder

  • ServletUriComponentsBuilder

用例:

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
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
//下面三位都是可以给request域中放数据
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");

Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");

Map<String,Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");//得出testParam方法赋予的值 world666
Object world = request.getAttribute("world");//得出testParam方法赋予的值 hello666
Object message = request.getAttribute("message");//得出testParam方法赋予的值 HelloWorld

map.put("reqMethod_msg",msg1);
map.put("annotation_msg",msg);
map.put("hello",hello);
map.put("world",world);
map.put("message",message);

return map;
}
  • Map<String,Object> map

  • Model model

  • HttpServletRequest request

上面三位都是可以给request域中放数据,用request.getAttribute()获取

接下来我们看看,Map<String,Object> mapModel model用什么参数处理器。

Map<String,Object> map参数用MapMethodProcessor处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return (Map.class.isAssignableFrom(parameter.getParameterType()) &&
parameter.getParameterAnnotations().length == 0);
}

@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
return mavContainer.getModel();
}

...

}

注意到

1
return mavContainer.getModel();

mavContainer.getModel()如下:

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
public class ModelAndViewContainer {

...

private final ModelMap defaultModel = new BindingAwareModelMap();

@Nullable
private ModelMap redirectModel;

...

public ModelMap getModel() {
if (useDefaultModel()) {
return this.defaultModel;
}
else {
if (this.redirectModel == null) {
this.redirectModel = new ModelMap();
}
return this.redirectModel;
}
}

private boolean useDefaultModel() {
return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
}
...

}

注意到

1
2
3
if (useDefaultModel()) {
return this.defaultModel;
}

查看this.defaultModel的类型是BindingAwareModelMap,进一步探究发现底层是

1
2
3
public class ExtendedModelMap extends ModelMap implements Model {
//..
}

BindingAwareModelMap既是Model也是Map

Model modelModelMethodProcessor处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return Model.class.isAssignableFrom(parameter.getParameterType());
}

@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
return mavContainer.getModel();
}
...
}

也是return mavContainer.getModel();。这里跟MapMethodProcessor方法内的处理办法一致。

最终也是使用BindingAwareModelMap类型,BindingAwareModelMap既是Model也是Map

查看BindingAwareModelMap的继承链:

Pasted image 20230125175143.png

来到InvocableHandlerMethod类的getMethodArgumentValues方法

查看args,可以看到Map和Model实际上是用同一个对象来处理的

Pasted image 20230125175559.png

一直“恢复程序执行”,直到来到ServletInvocableHandlerMethod#invokeAndHandle方法

查看mavContatiner的值,可以看到Map和Model实际上是用同一个对象来处理的,处理结果是图中的“hello”和"world"

Pasted image 20230125180305.png

目标方法执行完成

目标方法执行完成,将所有的数据都放在ModelAndViewContainer(mavContaioner).

mavContainer包含要去的地址url,还包含Model的数据

Pasted image 20230125182100.png

将数据填充到Model And View

步入getModelAndView方法

这个方法首先更新Model数据,

Pasted image 20230125182233.png

然后将数据填充到ModelAndView

1
2
3
4
5
6
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,  
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

//...
// 注意这句
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());

如果你的数据是重定向到新网页的,这个方法还会帮你把数据绑定到新网页的请求上下文(RequestContex),以完成旧网页request携带的内容转移到新网页request上。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Nullable  
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

// ...
// 注意这里
if (model instanceof RedirectAttributes redirectAttributes) {
Map<String, ?> flashAttributes = redirectAttributes.getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}

定位processDispatchResult方法

步出,回到org.springframework.web.servlet.DispatcherServlet#doDispatch,即DispatcherServlet类的doDispatch方法,

  • 后文全部参照这种类#方法的表示形式
1
2
3
4
5
6
7
8
9
10
  
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

//...
mappedHandler.applyPostHandle(processedRequest, response, mv);

// ...
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

现在ha.handle执行完成

1
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

方法继续往下执行,来到核心语句,这里执行拦截器的,后文会进行解析

1
mappedHandler.applyPostHandle(processedRequest, response, mv);

继续步过,来到核心语句,处理分发结果。这是下一小节的解析重点。

1
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

如何获取Map<String,Object> mapModel model的值

看看Map<String,Object> mapModel model值是如何做到用request.getAttribute()获取的。

根据上一小节的内容,所有的数据都放在 ModelAndView ,它包含要去的页面地址View,还包含Model数据。

回顾上一小节的末尾,先看ModelAndView接下来是如何处理的?

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
public class DispatcherServlet extends FrameworkServlet {

...

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...

try {
ModelAndView mv = null;

...

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

...

}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//处理分发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
...

}

它包含这三个核心语句

1
2
3
4
5
6
7
8
9
  
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

//...
mappedHandler.applyPostHandle(processedRequest, response, mv);

// ...
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

步入processDispatchResult方法,方法首先获取view的name,然后对view进行render,

1
2
3
4
5
6
7
8
9
10
11
12
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,  
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

// ...
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}

其中的核心语句:

1
render(mv, request, response); 

步入render方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {  
// ...

View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
//...
}

注意到核心语句:

1
2
3
// We need to resolve the view name.  
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

resolveViewName方法进入视图解析器的流程,以后会分析。

继续看render方法的后半段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {  

// ...
try {
if (mv.getStatus() != null) {
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}

// ...
}

注意到核心语句

1
view.render(mv.getModelInternal(), request, response);  

步入view.render方法,来到InternalResourceView#render方法

  • 这里的view属为InternalResourceView类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InternalResourceView extends AbstractUrlBasedView {

@Override//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {

//...

Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);

//核心语句
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

注意到核心语句:

1
2
3
4
5
6
// 创建一个要合并的输出Model
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
//...
// 核心语句
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

步入createMergedOutputModel方法,这个方法将Model数据转移到转移到mergedModel(即HashMap中)

1
2
3
4
5
6
7
8
9
10
11
12
protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model,  
HttpServletRequest request, HttpServletResponse response) {
// ...

// 把model里的数据全部放进mergedModel,转移到HashMap中
if (model != null) {
mergedModel.putAll(model);
}

//...

}

步出,回到InternalResourceView#render方法,核心语句如下:

1
2
3
4
5
6
// 创建一个要合并的输出Model
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
//...
// 核心语句
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

注意到核心语句:

1
2
// 核心语句
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

步入renderMergedOutputModel方法

1
2
3
4
5
6
7
8
9
   @Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

// Expose the model object as request attributes.
// 暴露模型作为请求域属性
exposeModelAsRequestAttributes(model, request);//<---重点
//...
}

注意到核心语句:

1
2
3
// Expose the model object as request attributes.
// 暴露模型作为请求域属性
exposeModelAsRequestAttributes(model, request);//<---重点

步入exposeModelAsRequestAttributes方法,方法遍历Model中的所有数据,将数据放在request域中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {

model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}

}

exposeModelAsRequestAttributes方法看出,

  • Map<String,Object> mapModel model这两种类型数据可以给request域中放数据,
    • 这个操作是在渲染viewview.render())时进行的,
    • 跳转操作发生在这之后
  • 使用数据时,用request.getAttribute()获取。

exposeModelAsRequestAttributes方法是业务逻辑的最底层了,再往下是工具类。

该方法的继承关系如下:

Pasted image 20230125200905.png

35、请求处理-【源码分析】-自定义参数绑定原理

假设有下面的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class ParameterTestController {

/**
* 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定
* @param person
* @return
*/
@PostMapping("/saveuser")
public Person saveuser(Person person){
return person;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {

private String userName;
private Integer age;
private Date birth;
private Pet pet;

}

@Data
public class Pet {

private String name;
private String age;

}

封装过程用到ServletModelAttributeMethodProcessor

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
55
56
57
58
59
60
61
62
63
64
65
66
public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {

@Override//本方法在ModelAttributeMethodProcessor类,
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}

@Override
@Nullable//本方法在ModelAttributeMethodProcessor类,
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

...

String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}

Object attribute = null;
BindingResult bindingResult = null;

if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
...
}
}

if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
//web数据绑定器,将请求参数的值绑定到指定的JavaBean里面**
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}

// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);

return attribute;
}
}

WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中

在过程当中,用到GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型

36、请求处理-【源码分析】-自定义Converter原理

未来我们可以给WebDataBinder里面放自己的Converter;

下面演示将字符串“啊猫,3”转换成Pet对象。

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
//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {

@Override
public Pet convert(String source) {
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}

SpringBootWeb开发-3

spring-boot-web-开发-3

参考、引用、致谢