alamide的笔记库「 87篇笔记 」「 小破站已建 0 天啦 🐶 」


SpringMVC Response

2023-05-16, by alamide

SpringMVC Response 相关

1.Controller 支持的返回数据类型

Controller method return value Description
@ResponseBody 返回的数据经由 HttpMessageConverter 转换,会依据 Content-Type 来转换
HttpEntity<B>, ResponseEntity<B> 返回由 HttpMessageConverter 转换,且包含 Header 信息
HttpHeaders 返回 Header 信息,没有响应体
String 视图名

2.@ResponseBody

@ResponseBody 可以加载类上和方法上,加在类上表示对所有方法都生效,加在类上可以用 @RestController(=@Controller + @ResponseBody) 代替,方法返回的数据通过 HttpMessageConverter 来转化

转换规则 HttpMessageConverter

3.ResponseEntity

@ResponseBody 多返回一个头信息和 Status,这个返回值可以解决使用 @ResponseBody 的一个小问题。使用 @ResponseBody 时,如下代码示例

@ResponseBody
@PostMapping("/json")
public UserInfo json(@RequestBody UserInfo userInfo) {
    return userInfo;
}

如上代码,我们想返回的内容是 UserInfojson,如果客户端指定 Accept: application/json,那么返回值没有问题,框架会依据 Accept 来挑选 MappingJackson2HttpMessageConverter 来转换。如果客户端没有指定 Accept,那么返回的内容就不可控了,会优先返回符合条件,排在最前面的 Converter,本例会返回 xml 格式的数据。你可能会想到指定 response.setContentType("application/json"),这里不会生效。

@PostMapping("/json")
public UserInfo json(@RequestBody UserInfo userInfo, HttpServletResponse response) {
    //无效,仍会返回 xml 格式的数据
    response.setContentType("application/json");
    return userInfo;
}

原因是,框架在封装 response 时,并没有将设置的 Content-Type 放到 Headers 中,所以无法正确的转换

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
		implements HandlerMethodReturnValueHandler {
    ...
    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
    ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
    throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        ...
        //为 null,无法获取到正确的 ContentType
        MediaType contentType = outputMessage.getHeaders().getContentType();
        ...
    }
    ...
}
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
		implements HandlerMethodReturnValueHandler {
    //创建 OutputMessage 即 ServletServerHttpResponse
    protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        //包装 response
        return new ServletServerHttpResponse(response);
    }
}

public class ServletServerHttpResponse implements ServerHttpResponse {

	private final HttpServletResponse servletResponse;

	private final HttpHeaders headers;

	private boolean headersWritten = false;

	private boolean bodyUsed = false;

	@Nullable
	private HttpHeaders readOnlyHeaders;

	//headers 为 empty
	public ServletServerHttpResponse(HttpServletResponse servletResponse) {
		Assert.notNull(servletResponse, "HttpServletResponse must not be null");
		this.servletResponse = servletResponse;
		this.headers = new ServletResponseHttpHeaders();
	}
    ...

    @Override
	public HttpHeaders getHeaders() {
		if (this.readOnlyHeaders != null) {
			return this.readOnlyHeaders;
		}
		else if (this.headersWritten) {
			this.readOnlyHeaders = HttpHeaders.readOnlyHttpHeaders(this.headers);
			return this.readOnlyHeaders;
		}
		else {
            //empty
			return this.headers;
		}
	}
    ...
}

现在使用 ResponseEntity 就可以很好的解决这个问题了

@GetMapping("/pet")
public ResponseEntity<Pet> handle(){
    Pet pet = new Pet(1L, 3L, "tom");
    return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(pet);
}

原理

public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
    ...
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        ...
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
		ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
        ...
        HttpHeaders outputHeaders = outputMessage.getHeaders();
        HttpHeaders entityHeaders = httpEntity.getHeaders();
        //将设置的 Content-Type 复制到 outputMessage 的 Header 中,最终解析时可以获取到 MediaType 为 application/json,可以转换为目标类型
        if (!entityHeaders.isEmpty()) {
            entityHeaders.forEach((key, value) -> {
            if (HttpHeaders.VARY.equals(key) && outputHeaders.containsKey(HttpHeaders.VARY)) {
                List<String> values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders);
                if (!values.isEmpty()) {
                    outputHeaders.setVary(values);
                }
                }
                else {
                    outputHeaders.put(key, value);
                }
            });
        }
    }
    ...
}

4.Jackson JSON

使用 @JsonView ,屏蔽一些敏感信息

@AllArgsConstructor
@NoArgsConstructor
public class User {
    public interface WithoutPassword{}
    public interface WithPassword extends WithoutPassword{}

    private String username;

    private String password;

    @JsonView(WithPassword.class)
    public String getPassword() {
        return password;
    }

    @JsonView(WithoutPassword.class)
    public String getUsername() {
        return username;
    }
}

//屏蔽密码信息
@JsonView(User.WithoutPassword.class)
@ResponseBody
@GetMapping("/user")
public User getUser(){
    return new User("alamide", "123456");
}

5.HttpHeaders

返回响应头信息,而没有响应体

@GetMapping("/header")
public HttpHeaders header(){
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
    return httpHeaders;
}

6.String 视图

SpringMVC 使用 ViewResolver 来实现视图映射,通过视图名映射到具体的页面,要实现映射要注册具体的映射器。现在前后端分离的场景居多,所以这里简略学习一下。

@Configuration
@ComponentScan(basePackages = {"com.alamide.web"})
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        final UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
        UrlBasedViewResolverRegistration resolverRegistration = new UrlBasedViewResolverRegistration(viewResolver);
        resolverRegistration.prefix("/WEB-INF/").suffix(".jsp").viewClass(InternalResourceView.class);
        registry.viewResolver(viewResolver);
    }
}

映射

@GetMapping("/view")
public String jsp(Model model){
    model.addAttribute("message", "this is a message!");
    return "hello";
}

hello.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>Title</title>
  </head>
  <body>
    <!--从 Model 中直接获取-->
    ${message}
  </body>
</html>

最终返回具体的页面

Tags: Java - SpringMVC
~ belongs to alamide@163.com