概述
官方解释:
可以配置SpringMVC如何根据请求确定请求的媒体类型。可用选项包括检查文件扩展名的URL路径、检查“accept”头、特定查询参数,或者在不请求任何内容时返回默认内容类型。默认情况下,首先检查请求URI中的路径扩展,然后检查“accept”头。
个人理解:
所谓内容协商,其实就是根据客户端请求的url扩展后缀、请求参数或者请求头来指定响应内容的类型。
1.案例
根据请求后缀的不同返回不同的视图 ,/accounts.htm返回htm页面,/accounts.xls返回表格,最简单的做法是:
@Controller class AccountController { @RequestMapping("/accounts.htm") public String listAsHtml(Model model, Principal principal) { // Duplicated logic model.addAttribute( accountManager.getAccounts(principal) ); return ¨accounts/list¨; // View determined by view-resolution } @RequestMapping("/accounts.xls") public AccountsExcelView listAsXls(Model model, Principal principal) { // Duplicated logic model.addAttribute( accountManager.getAccounts(principal) ); return new AccountsExcelView(); // Return view explicitly } }
但是使用多个方法是不优雅的,会破坏MVC模式,如果我们也想支持其他数据格式(比如pdf、csv等其他格式),那么我们将会需要每种格式都要有一份类似的逻辑,这将严重违背java中抽象和复用的原则。
2.期望
对于相同的逻辑,而只是返回结果或者视图的不同,使用同一段逻辑根据客户端请求的后缀、参数或者请求头的不同返回个性化响应。
3.引入内容协商器CNVR
ContentNegotiatingViewResolver简称CNVR。基于请求文件名或接受头解析视图的ViewResolver的实现。ContentNegotiangViewResolver不解析视图本身,而是委托给其他视图解析器。默认情况下,这些其他解析器是从应用程序上下文中自动获取的,尽管也可以使用ViewResolver属性显式设置它们。需要注意的是,为了使此视图解析器正常工作,需要将order属性设置为比其他属性更高的优先级(默认值为Ordered.HIGHEST_PRECEDENCE)。
此视图解析器使用请求的媒体类型为请求选择合适的视图。请求的媒体类型是通过配置的ContentNegotiationManager确定的。确定请求的媒体类型后,此视图解析器将查询每个委托视图解析器中的某个视图,并确定请求的媒体类型是否与该视图的内容类型兼容,并返回最合适的视图。
此外,此视图解析器公开了DefaultView属性,允许你重写视图解析器提供的视图。注意,这些默认视图是作为候选视图提供的,并且仍然需要请求内容类型(通过文件扩展名、参数或接受头,如上所述)。
例如,如果请求路径为/view.html,则此视图解析器将查找text/html内容类型的视图(基于HTML文件扩展名)。带有text/html请求接受头的请求/view具有相同的结果。
3.1:工作原理
CNVR作为一个代理视图解析器,其接收到请求时候会委托给spring容器中配置的其他视图解析器处理并返回具体的视图,工作原理大致如下:
3.2:时序图
从接收一个普通的请求到处理完逻辑返回结果给客户端,在spring内部的核心流程时序图如下:
4.三种内容协商策略及实现
spring支持三种内容协商策略:
在默认情况下,Spring的内容协商策略管理器(ContentNegotiationManager)会尝试使用这三种策略,如果以上三种策略都没有被启用的话,我们可以定义一个默认的内容类型。
4.1:前置准备
4.1.1 pom中引入基本依赖
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> </dependency> <dependency> <groupId>net.sf.supercsv</groupId> <artifactId>super-csv</artifactId> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> </dependency>
4.1.2 请求处理器
@Controller public class ExportController { @Autowired private UserService userService; @GetMapping("/views") public String getUsers(ModelMap model) { model.addAttribute("users", userService.findAllUsers()); return "views"; } }
4.1.3视图解析器
4.1.3.1 视图
抽象视图:
public abstract class AbstractCsvView extends AbstractView { private static final String CONTENT_TYPE = "text/csv"; public AbstractCsvView() { setContentType(CONTENT_TYPE); } @Override protected boolean generatesDownloadContent() { return true; } @Override protected final void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType(getContentType()); buildCsvDocument(model, request, response); } protected abstract void buildCsvDocument( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception; }
具体视图:
public class CsvView extends AbstractCsvView { @Override protected void buildCsvDocument(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setHeader("Content-Disposition", "attachment; filename=\"my-csv-file.csv\""); List<User> users = (List<User>) model.get("users"); String[] header = {"id", "name", "createTime", "sex", "age"}; ICsvBeanWriter csvWriter = new CsvBeanWriter(response.getWriter(), CsvPreference.STANDARD_PREFERENCE); csvWriter.writeHeader(header); for (User user : users) { csvWriter.write(user, header); } csvWriter.close(); } }
4.1.3.2 解析器
public class CsvViewResolver implements ViewResolver { @Nullable @Override public View resolveViewName(String s, Locale locale) throws Exception { return new CsvView(); } }
4.1.4内容协商配置
public class WebConfig implements WebMvcConfigurer { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { Map<String,MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("excel",MediaType.parseMediaType("application/vnd.ms-excel")); mediaTypes.put("csv",MediaType.parseMediaType("text/csv")); mediaTypes.put("pdf",MediaType.parseMediaType("application/pdf")); configurer.ignoreAcceptHeader(true) .favorPathExtension(true) .defaultContentType(MediaType.TEXT_HTML) .favorParameter(false) .parameterName("type") .mediaTypes(mediaTypes) ; } @Bean public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); resolver.setContentNegotiationManager(manager); resolver.setOrder(1); // Define all possible view resolvers List<ViewResolver> resolvers = new ArrayList<>(); resolvers.add(csvViewResolver()); resolvers.add(excelViewResolver()); resolvers.add(pdfViewResolver()); resolvers.add(jsonViewResolver()); resolver.setViewResolvers(resolvers); List<View> defaultViews = new ArrayList<>(); defaultViews.add(new CsvView()); defaultViews.add(new MappingJackson2JsonView()); defaultViews.add(new ExcelView()); defaultViews.add(new PdfView()); resolver.setDefaultViews(defaultViews); return resolver; } @Bean public ViewResolver excelViewResolver() { return new ExcelViewResolver(); } @Bean public ViewResolver csvViewResolver() { return new CsvViewResolver(); } @Bean public ViewResolver pdfViewResolver() { return new PdfViewResolver(); } @Bean public ViewResolver jsonViewResolver() { return new JsonViewResolver(); } }
这个配置类比较重要,此处做一下详细解析。WebConfig实现了WebMvcConfigurer接口,然后对于configureContentNegotiation方法进行覆盖,来进行默认内容类型的修改,我们具体看一下发生了什么:
然后我们通过contentNegotiatingViewResolver方法自定义了一个内容协商器并注入到spring容器中,设置了ContentNegotiationManager,优先级,代理的视图解析器以及默认支持的视图。
4.1.5 应用启动器
@SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
4.2:三种内容协商策略实现
4.2.1 后缀模式
后缀模式就是发送请求的是加上类似.json(.xml,.csv,.pdf等)的后缀。
4.2.1.1 配置支持
在WebConfig配置的 configureContentNegotiation方法中启用后缀匹配,为了不受其他策略的烦扰,禁用其他两种模式(设置默认响应类型为JSON):
configurer.ignoreAcceptHeader(false) .favorPathExtension(true) .favorParameter(false) .defaultContentType(MediaType.APPLICATION_JSON);
4.2.1.2 测试验证
发送请求:
curl http://localhost:8080/views
可以看到没有带后缀的请求走了兜底的默认响应类型:
发送带后缀的请求:
curl http://localhost:8080/views.csv
从结果中可以看出返回了csv表格类型的响应:
从浏览器发送请求的话会下载csv文件,内容和上述一致:
发送参数和请求头模式的请求找不到合适的视图解析器直接走默认响应类型:
4.2.2查询参数模式
查询参数模式中默认的参数名是format,可以在设置启用参数查询模式的基础上设置自定义parameterName。
4.2.2.1 配置支持
启用参数查询并且关闭后缀模式和请求头模式,并且自定义参数名为type:
configurer.ignoreAcceptHeader(true) .favorPathExtension(false) .favorParameter(true) .parameterName("type") .defaultContentType(MediaType.APPLICATION_JSON);
4.2.2.2 测试验证
浏览器中发送请求:
http://localhost:8080/views?type=pdf
下载文件后看到结果:
发送后缀和请求头模式的请求找不到合适的视图解析器直接走默认响应类型:
4.2.3请求头模式
请求头模式中加入Accept:application/*类似的内容,内容协商器会解析出来然后路由到指定的视图解析器。
4.2.3.1 配置支持
启用请求头模式并且关闭后缀模式和参数模式:
configurer.ignoreAcceptHeader(false) .favorPathExtension(false) .favorParameter(false) .defaultContentType(MediaType.APPLICATION_JSON);
4.2.3.2 测试验证
发送请求:
curl -H "Accept:application/json" http://localhost:8080/views
可以看到响应结果:
发送后缀和参数模式的请求找不到合适的视图解析器直接走默认响应类型:
4.3:三种内容协商策略优先级
对于上述三种模式的内容线上策略,在有些特定场景,我们可能会同时开启,这时候接收到请求的时候, 内容协商器CNVR具体路由到哪一个视图解析器就会涉及到优先级问题,多说无益,我们直接通过测试结果来得出三种策略同时开启是的执行优先级。
4.3.1请求同时带后缀、参数和请求头
使用命令行发送请求:
curl -H "Accept:application/json" http://localhost:8080/views.csv?type=pdf
响应结果如下:
可以明显地看出走的是后缀匹配模式。①也就是说如果三种内容内容协商模式都开启并且请求参数中包含三种模式的内容情况下,会优先走后缀策略模式。
4.3.2请求同时带后缀和参数
发送请求:
curl http://localhost:8080/views.csv?type=pdf
响应结果如下:
可以明显地看出走的是后缀匹配模式。②也就是说如果后缀模式和参数模式都开启并且请求参数中包含这种模式的内容情况下,会优先走后缀策略模式。
4.3.3请求同时带后缀和请求头
发送请求:
curl -H "Accept:application/json" http://localhost:8080/views.csv
响应结果如下:
可以明显地看出走的是后缀匹配模式。③也就是说如果后缀模式和请求头模式都开启并且请求参数中包含这种模式的内容情况下,会优先走后缀策略模式。
4.3.4请求同时带参数和请求头
发送请求:
curl -H "Accept:application/pdf" http://localhost:8080/views?type=csv
响应结果:
从结果中可以看出走的是参数模式。④也就是说如果参数模式和请求头模式都开启并且请求参数中包含这种模式的内容情况下,会优先走参数策略模式。
4.3.5结论
从上述①②③④结论中,我们可以得出在三种内容内容协商模式都开启的情况下,内容协商器对于三种策略模式执行的优先级顺序是(从高到低):
后缀模式->参数模式->请求头模式
总结
此篇文章我们详细介绍了spring内容协商的概念、用法和原来,并且通过实例代码的方式验证了三种策略模式执行的优先级,相信大家对spring内容协商有了一个大致的了解,对于内容协商模式的作用和具体使用场景,大家可以相互讨论或者翻阅网上相关资料,文章中如有纰漏或者描述不准确的地方还请斧正。
参考资料
https://spring.io/blog/2013/06/03/content-negotiation-using-views
https://spring.io/blog/2013/05/11/content-negotiation-using-spring-mvc/
https://www.javadevjournal.com/spring-mvc/spring-mvc-content-negotiation/
https://junq.io/spring-mvc%E5%AE%9E%E7%8E%B0http%E5%86%85%E5%AE%B9%E5%8D%8F%E5%95%86-content-negotiation.html
https://www.baeldung.com/spring-mvc-content-negotiation-json-xml
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。