Spring MVC 支持的视图
- Jackson-based JSON / XML
- Thymeleaf & FreeMarker
用的比较多的是Jackson-based JSON、XML,后台更多的是提供数据为主。在做boss系统的时候,并不需要绚丽的前端,我们使用模板引擎来做页面在系统上。springboot提供了一些springmvc支持的模板引擎Thymeleaf & FreeMarker。
配置 MessageConverter
- 通过 WebMvcConfigurer 的 configureMessageConverters()配置
• Spring Boot 自动查找 HttpMessageConverters 进行注册
springboot提供了简单的方法,比如说,在应用程序上下文当中,去寻找HttpMessageConverters 并自动把它注册进去。下面,为springmvc当中,自动配置Jackson和JSON / XML的示例:
public class WebConfiguration implements WebMvcConfigurer{
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters){
Jackson2ObjectMapperBuilder bulider = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(bulider.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(bulider.createXmlMapper(true).build()));
}
}
通过converters.add的这样一个方法,增加了两个Converter进去。
Spring Boot 对 Jackson 的支持
- JacksonAutoConfiguration
• Spring Boot 通过 @JsonComponent(序列化器和反序列化器) 注册 JSON 序列化组件
• Jackson2ObjectMapperBuilderCustomizer (自定义器,在spring的上下文当中,去注册一个Customizer类型的bean) - JacksonHttpMessageConvertersConfiguration
• 增加 jackson-dataformat-xml 以支持 XML 序列化
我们进入WebMvcAutoConfiguration类中,找到WebMvcAutoConfigurationAdapter方法。
springboot给我们做好了一个配置类configureMessageConverters。
将spring上下文中Converter存到HttpMessageConverter。然后通过converters.addAll进行配置。
在configureMessageConverters中,我们可以看到springboot给我们配了许多的 ViewResolver()。
我们进入JacksonAutoConfiguration类。
在里面添加了JsonComponentModule的一个Module,这个Module处理JsonComponent注解
我们进入JsonComponentModule。在addJsonBean中,将Serializer添加进去。
例子代码
@Controller
@RequestMapping("/coffee")
@Slf4j
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
// @PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
// @ResponseBody
// @ResponseStatus(HttpStatus.CREATED)
// public Coffee addCoffee(@Valid NewCoffeeRequest newCoffee,
// BindingResult result) {
// if (result.hasErrors()) {
// // 这里先简单处理一下,后续讲到异常处理时会改
// log.warn("Binding Errors: {}", result);
// return null;
// }
// return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
// }
@PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
@ResponseStatus(HttpStatus.CREATED)
public Coffee addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee) {
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) //这里 接收的是一个JSON类型
@ResponseBody
@ResponseStatus(HttpStatus.CREATED) //以Json方式来做Coffee的一个新增咖啡的方法
public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) { //将Request的内容绑到newCoffee上面 用Valid注解 替我们做一个校验
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseBody
@ResponseStatus(HttpStatus.CREATED)
public List<Coffee> batchAddCoffee(@RequestParam("file") MultipartFile file) {
List<Coffee> coffees = new ArrayList<>();
if (!file.isEmpty()) {
BufferedReader reader = null;
try {
reader = new BufferedReader(
new InputStreamReader(file.getInputStream()));
String str;
while ((str = reader.readLine()) != null) {
String[] arr = StringUtils.split(str, " ");
if (arr != null && arr.length == 2) {
coffees.add(coffeeService.saveCoffee(arr[0],
Money.of(CurrencyUnit.of("CNY"),
NumberUtils.createBigDecimal(arr[1]))));
}
}
} catch (IOException e) {
log.error("exception", e);
} finally {
IOUtils.closeQuietly(reader);
}
}
return coffees;
}
@GetMapping(path = "/", params = "!name")
@ResponseBody
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
@RequestMapping(path = "/{id}", method = RequestMethod.GET)
@ResponseBody
public Coffee getById(@PathVariable Long id) { // 即可以返回json类型的 也可以返回xml类型的
Coffee coffee = coffeeService.getCoffee(id);
log.info("Coffee {}:", coffee);
return coffee;
}
@GetMapping(path = "/", params = "name")
@ResponseBody
public Coffee getByName(@RequestParam String name) {
return coffeeService.getCoffee(name);
}
}
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
// 增加了jackson-datatype-hibernate5就不需要这个Ignore了
//@JsonIgnoreProperties(value = {"hibernateLazyInitializer"}) 可以支持这个类的代理类 在此标记不生成json对象的属性
//
//因为jsonplugin用的是java的内审机制.hibernate会给被管理的pojo加入一个hibernateLazyInitializer属性,jsonplugin会把hibernateLazyInitializer也拿出来操作,并读取里面一个不能被反射操作的属性就产生了这个异常.
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
@UpdateTimestamp
private Date updateTime;
}
@JsonComponent //springboot自动的将我们的序列化器和反序列化器 这个是序列化
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}
@JsonComponent //反序列化
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
public class WaiterServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
@Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
} //这是个Jackson的模块 springboot自动帮我们注册进去
//@Bean
//public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
// return builder -> builder.indentOutput(true); 对builder做一个输出 我们希望它有一个缩进 这行代码做了这个接口的内部匿名实现类
//}
}
结果
查询咖啡
增加了一个json对应的序列化器之后,price返回的是一个数字
如果我们希望返回的是一个xml,我们需要加个头
使用getbyid查询咖啡
创建咖啡
Content-Type为application/json
使用 Thymeleaf 模板引擎
一共有两种配置
添加 Thymeleaf 依赖
• org.springframework.boot:spring-boot-starter-thymeleaf
在start.spring.io中勾选
Spring Boot 的自动配置
- ThymeleafAutoConfiguration
• ThymeleafViewResolver
springboot中,有ThymeleafAutoConfiguration 类完成相关配置。
Thymeleaf 的一些默认配置
• spring.thymeleaf.cache=true
模板可以被缓存
• spring.thymeleaf.check-template=true
是否校验模板
• spring.thymeleaf.check-template-location=true
是否检查模板位置是否正确
• spring.thymeleaf.enabled=true
是否启用MVC-Thymeleaf视图
• spring.thymeleaf.encoding=UTF-8
采用utf-8编码,避免页面乱码
• spring.thymeleaf.mode=HTML
页面是HTML类型。
• spring.thymeleaf.servlet.content-type=text/html
文档MIME类型是text/html,也就是html格式的文档。
• spring.thymeleaf.prefix=classpath:/templates/
表示html页面在resources文件夹下的templates文件夹中。即为页面前缀
• spring.thymeleaf.suffix=.html
表示页面后缀是html格式
例子
目录如下:
主要修改的方法为CoffeeOrderController
@Controller //不光有Request接口 还有与视图相关 modelandview有视图的方法
@RequestMapping("/order")
@Slf4j
public class CoffeeOrderController {
@Autowired
private CoffeeOrderService orderService;
@Autowired
private CoffeeService coffeeService;
@GetMapping("/{id}")
@ResponseBody //没有了@RestController 需要加@ResponseBody
public CoffeeOrder getOrder(@PathVariable("id") Long id) {
return orderService.get(id);
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
@ResponseStatus(HttpStatus.CREATED)
public CoffeeOrder create(@RequestBody NewOrderRequest newOrder) {
log.info("Receive new Order {}", newOrder);
Coffee[] coffeeList = coffeeService.getCoffeeByName(newOrder.getItems())
.toArray(new Coffee[] {});
return orderService.createOrder(newOrder.getCustomer(), coffeeList);
}
@ModelAttribute // List会作为model的一个属性传入
public List<Coffee> coffeeList() {
return coffeeService.getAllCoffee();
}
@GetMapping(path = "/")//访问根路径 会去呈现一个视图 会拼上suffix和prefix属性的值 变成一个具体的视图的模板的位置
public ModelAndView showCreateForm() {
return new ModelAndView("create-order-form");
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) //form表单的形式
public String createOrder(@Valid NewOrderRequest newOrder,
BindingResult result, ModelMap map) { //做表单的绑定和校验
if (result.hasErrors()) { //检查绑定的结果是否有错误
log.warn("Binding Result: {}", result);
map.addAttribute("message", result.toString());//将错误信息放到modelandview当中
return "create-order-form"; //返回视图名 做视图呈现
}
log.info("Receive new Order {}", newOrder);//新建订单
Coffee[] coffeeList = coffeeService.getCoffeeByName(newOrder.getItems())
.toArray(new Coffee[] {});
CoffeeOrder order = orderService.createOrder(newOrder.getCustomer(), coffeeList);
return "redirect:/order/" + order.getId(); //做重定向 访问getOrder方法
}
}
create-order-form.html代码:
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Coffee Available Today</h2>
<table>
<thead>
<tr>
<th>Coffee Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr th:each="coffee : ${coffeeList}">
<td th:text="${coffee.name}">Espresso</td>
<td th:text="${coffee.price}">CNY 12.0</td>
</tr>
</tbody>
</table>
<h2>Your Order</h2>
<form action="#" th:action="@{/order/}" method="post">
<label>Customer Name</label>
<input type="text" name="customer" />
<ul>
<li th:each="coffee : ${coffeeList}">
<input type="checkbox" name="items" th:value="${coffee.name}" />
<label th:text="${coffee.name}">Espresso</label>
</li>
</ul>
<input type="submit" value="Submit"/>
<p th:text="${message}">Message</p>
</form>
</body>
</html>
访问http://localhost:8080/order/,并创建订单:
我们可以看到,载入界面之后,会根据coffeeList方法,打印出所有的coffee,提交之后为:
提交成功之后,重定向到redirect:/order/id 上并访问getOrder方法。
如果提交错误,将会把错误信息打印出来:
此错误为没有选择咖啡。