Spring MVC
认识 Spring MVC
DispatcherServlet (核心、也是所有请求的入口)
- Controller:业务处理逻辑
- xxxResolver:解析器
ViewResolver
HandlerExceptionResolver
MultipartResolver
- HandlerMapping:Request 与 Controller 的映射。
Spring MVC 中的常用注解
- @Controller
- @RestController :@Controller + @RequestBody。
- @RequestMapping:此 Controller 要处理哪些请求。
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
- @RequestBody:请求报文体
- @ResponseBody:响应报文体
- @ResponseStatus:响应状态码
定义一个简单的 Controller
CoffeeController
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequestMapping("/coffee")
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
@GetMapping("/")
@ResponseBody
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
}
CoffeeOrderController
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewOrderRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
@Slf4j
public class CoffeeOrderController {
@Autowired
private CoffeeOrderService orderService;
@Autowired
private CoffeeService coffeeService;
@PostMapping("/")
@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);
}
}
NewOrderRequest
package geektime.spring.springbucks.waiter.controller.request;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Getter
@Setter
@ToString
public class NewOrderRequest {
private String customer;
private List<String> items;
}
CoffeeService
package geektime.spring.springbucks.waiter.service;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@CacheConfig(cacheNames = "CoffeeCache")
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
@Cacheable
public List<Coffee> getAllCoffee() {
return coffeeRepository.findAll(Sort.by("id"));
}
public List<Coffee> getCoffeeByName(List<String> names) {
return coffeeRepository.findByNameInOrderById(names);
}
}
启动类
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
public class WaiterServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
}
测试(RestServices)
* Found 2 services
* waiter-service
* /coffee/
* /order/
/**
* /coffee/
* @return CoffeeOrder
*/
// http://localhost:8080/coffee/
/**
* /order/
* @return CoffeeOrder
*/
// RequestBody
{
"customer": "demoData",
"items": [
"demoData"
]
}
//Response
{
"id": 1,
"createTime": "2020-01-19T06:30:04.445+0000",
"updateTime": "2020-01-19T06:30:04.445+0000",
"customer": "demoData",
"items": [],
"state": "INIT"
}
理解 Spring 的应用上下文
关于上下文常用的接口及其实现
- BeanFactory :
DefaultListableBeanFactory
- ApplicationContext:
ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、AnnotationConfifigApplicationContext
- WebApplicationContext:Web 相关上文接口。
示例
要增强的 Bean TestBean
package geektime.spring.web.context;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@AllArgsConstructor
@Slf4j
public class TestBean {
private String context;
public void hello() {
log.info("hello " + context);
}
}
切面 FooAspect
package geektime.spring.web.foo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
@Aspect
@Slf4j
public class FooAspect {
@AfterReturning("bean(testBean*)")
public void printAfter() {
log.info("after hello()");
}
}
配置类 FooConfig
package geektime.spring.web.foo;
import geektime.spring.web.context.TestBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class FooConfig {
@Bean
public TestBean testBeanX() {
return new TestBean("foo");
}
@Bean
public TestBean testBeanY() {
return new TestBean("foo");
}
@Bean
public FooAspect fooAspect() {
return new FooAspect();
}
}
applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- <aop:aspectj-autoproxy/>-->
<bean id="testBeanX" class="geektime.spring.web.context.TestBean">
<constructor-arg name="context" value="Bar" />
</bean>
<!--<bean id="fooAspect" class="geektime.spring.web.foo.FooAspect" />-->
</beans>
启动类
package geektime.spring.web.foo;
import geektime.spring.web.context.TestBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class FooConfig {
@Bean
public TestBean testBeanX() {
return new TestBean("foo");
}
@Bean
public TestBean testBeanY() {
return new TestBean("foo");
}
@Bean
public FooAspect fooAspect() {
return new FooAspect();
}
}
总结
FooConfig.java:父上下文(parent application context)。
applicationContext.xml:子上下文(child application context)。
FooConfig.java 中定义两个 testBean,分别为 testBeanX(foo) 和 testBeanY(foo)。
applicationContext.xml 定义了一个 testBeanX(bar)。
委托机制:在自己的 context 中找不到 bean,会委托父 context 查找该 bean。
场景一:
父上下文开启 @EnableAspectJAutoProxy 的支持
子上下文未开启 <aop: aspectj-autoproxy />
切面 fooAspect 在 FooConfig.java 定义(父上下文增强)
输出结果:
testBeanX(foo) 和 testBeanY(foo) 均被增强。
testBeanX(bar) 未被增强。
结论:
在父上下文开启了增强,父的 bean 均被增强,而子的 bean 未被增强。
场景二:
父上下文开启 @EnableAspectJAutoProxy 的支持
子上下文开启 <aop: aspectj-autoproxy />
切面 fooAspect 在 applicationContext.xml 定义(子上下文增加)
输出结果:
testBeanX(foo) 和 testBeanY(foo) 未被增强。
testBeanX(bar) 被增强。
结论:
在子上下文开启增强,父的 bean 未被增强,子的 bean 被增强。
// 根据场景一和场景二的结果,有结论:
“各个 context 相互独立,每个 context 的 aop 增强只对本 context 的 bean 生效”。如果想将切面配置成通用的,对父和子上下文的 bean 均支持增强,则:
1. 切面 fooAspect 定义在父上下文。
2. 父上下文和子上下文,均要开启 aop 的增加,即 @EnableAspectJAutoProxy 或<aop: aspectj-autoproxy /> 的支持。
Spring MVC 的请求处理流程
一个请求的大致处理流程
- 绑定 ⼀些 Attribute:WebApplicationContext / LocaleResolver / ThemeResolver
- 处理 Multipart:如果是,则将请求转为 MultipartHttpServletRequest
- Handler 处理:如果找到对应 Handler,执 ⾏ Controller 及前后置处理器逻辑
- 处理返回的 Model ,呈现视图。
如何定义处理方法
定义映射关系
@Controller
@RequestMapping
- path / method 指定映射路径与 ⽅法
- params / headers 限定映射范围
- consumes / produces 限定请求与响应格式
一些快捷方式
- @RestController
- @GetMapping / @PostMapping / @PutMapping / @DeleteMapping / @PatchMapping
定义处理方法
- @RequestBody / @ResponseBody / @ResponseStatus
- @PathVariable / @RequestParam / @RequestHeader
- HttpEntity / ResponseEntity
方式示例
CoffeeOrderController
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewOrderRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.model.CoffeeOrder;
import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
@Slf4j
public class CoffeeOrderController {
@Autowired
private CoffeeOrderService orderService;
@Autowired
private CoffeeService coffeeService;
@GetMapping("/{id}")
public CoffeeOrder getOrder(@PathVariable("id") Long id) {
return orderService.get(id);
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@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);
}
}
CoffeeController
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
@RequestMapping("/coffee")
public class CoffeeController {
@Autowired
private CoffeeService coffeeService;
@GetMapping(path = "/", params = "!name")
@ResponseBody
public List<Coffee> getAll() {
return coffeeService.getAllCoffee();
}
@RequestMapping(path = "/{id}", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Coffee getById(@PathVariable Long id) {
Coffee coffee = coffeeService.getCoffee(id);
return coffee;
}
@GetMapping(path = "/", params = "name")
@ResponseBody
public Coffee getByName(@RequestParam String name) {
return coffeeService.getCoffee(name);
}
}
Spring MVC 支持的视图
JSON视图
- Jackson-based JSON / XML
- Thymeleaf & FreeMarker
配置 MessageConverter
- 通过 WebMvcConfigurer 的 configureMessageConverters()
• Spring Boot ⾃动查找 HttpMessageConverters 进⾏注册
Spring Boot 对 Jackson 的支持
- JacksonAutoConfiguration
• Spring Boot 通过 @JsonComponent 注册 JSON 序列化组件
• Jackson2ObjectMapperBuilderCustomizer
JacksonHttpMessageConvertersConfiguration
- 增加 jackson-dataformat-xml 以⽀持 XML 序列化
示例
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>waiter-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>waiter-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>6.0.1.GA</version>
</dependency>
<!-- 增加Jackson的Hibernate类型支持 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
<version>2.9.8</version>
</dependency>
<!-- 增加Jackson XML支持 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
BaseEntity
package geektime.spring.springbucks.waiter.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.util.Date;
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
// 增加了jackson-datatype-hibernate5就不需要这个Ignore了
//@JsonIgnoreProperties(value = {"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;
}
MoneySerializer
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
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());
}
}
MoneyDeserializer
package geektime.spring.springbucks.waiter.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@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());
}
}
CoffeeController
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.request.NewCoffeeRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@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)
@ResponseBody
@ResponseStatus(HttpStatus.CREATED)
public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) {
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) {
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);
}
}
启动类
package geektime.spring.springbucks.waiter;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching
public class WaiterServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
@Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
/**
* 开启缩进显示:Terminal 命令使用的时候
* @return
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
return builder -> builder.indentOutput(true);
}
}
Spring MVC 的异常解析
HandlerExceptionResolver(核心接口):常见的实现类如下
- SimpleMappingExceptionResolver
- DefaultHandlerExceptionResolver
- ResponseStatusExceptionResolver
- ExceptionHandlerExceptionResolver
异常处理方法
@* ExceptionHandler:
添加位置
- @Controller / @RestController
- @ControllerAdvice / @RestControllerAdvice
示例
FormValidationException 自定义异常
package geektime.spring.springbucks.waiter.controller.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
@Getter
@AllArgsConstructor
public class FormValidationException extends RuntimeException {
private BindingResult result;
}
GlobalControllerAdvice 全局异常处理
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.exception.FormValidationException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalControllerAdvice {
@ExceptionHandler(ValidationException.class)//专门用于处理ValidationException异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> validationExceptionHandler(ValidationException exception) {
Map<String, String> map = new HashMap<>();
map.put("message", exception.getMessage());
return map;
}
}
NewCoffeeRequest
package geektime.spring.springbucks.waiter.controller.request;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.joda.money.Money;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@ToString
public class NewCoffeeRequest {
@NotEmpty
private String name;
@NotNull
private Money price;
}
CoffeeController
package geektime.spring.springbucks.waiter.controller;
import geektime.spring.springbucks.waiter.controller.exception.FormValidationException;
import geektime.spring.springbucks.waiter.controller.request.NewCoffeeRequest;
import geektime.spring.springbucks.waiter.model.Coffee;
import geektime.spring.springbucks.waiter.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import javax.validation.ValidationException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@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);
throw new FormValidationException(result);
}
return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
}
@PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
@ResponseStatus(HttpStatus.CREATED)
public Coffee addJsonCoffee(@Valid @RequestBody NewCoffeeRequest newCoffee,
BindingResult result) {
if (result.hasErrors()) {
log.warn("Binding Errors: {}", result);
throw new ValidationException(result.toString());
}
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)
// @ResponseBody
// @ResponseStatus(HttpStatus.CREATED)
// public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) {
// 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) {
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);
}
}
Spring MVC 的拦截器
HandlerInteceptor(核心接口):实现类如下
- boolean preHandle() : 权限验证等……
- void postHandle()
- void afterCompletion()
针对 @ResponseBody 和 ResponseEntity 的情况
- ResponseBodyAdvice
针对异步请求的接口
- AsyncHandlerInterceptor:void afterConcurrentHandlingStarted()
拦截器的配置方式
- 常规方法:WebMvcConfigurer.addInterceptors()
Spring Boot 中的配置
- 创建⼀个带 @Configuration 的 WebMvcConfigurer 配置类
- 不能带 @EnableWebMvc(想彻底自己控制 MVC 配置除外)
示例
PerformanceInteceptor
package geektime.spring.springbucks.waiter.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class PerformanceInteceptor implements HandlerInterceptor {
private ThreadLocal<StopWatch> stopWatch = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StopWatch sw = new StopWatch();
stopWatch.set(sw);
sw.start();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
stopWatch.get().stop();
stopWatch.get().start();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
StopWatch sw = stopWatch.get();
sw.stop();
String method = handler.getClass().getSimpleName();
if (handler instanceof HandlerMethod) {
String beanType = ((HandlerMethod) handler).getBeanType().getName();
String methodName = ((HandlerMethod) handler).getMethod().getName();
method = beanType + "." + methodName;
}
log.info("{};{};{};{};{}ms;{}ms;{}ms", request.getRequestURI(), method,
response.getStatus(), ex == null ? "-" : ex.getClass().getSimpleName(),
sw.getTotalTimeMillis(), sw.getTotalTimeMillis() - sw.getLastTaskTimeMillis(),
sw.getLastTaskTimeMillis());
stopWatch.remove();
}
}
WaiterServiceApplication 启动类
package geektime.spring.springbucks.waiter;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import geektime.spring.springbucks.waiter.controller.PerformanceInteceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.TimeZone;
@SpringBootApplication//@SpringBootConfiguration 相当于 @Configuration
@EnableJpaRepositories
@EnableCaching
public class WaiterServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(WaiterServiceApplication.class, args);
}
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PerformanceInteceptor())
.addPathPatterns("/coffee/**").addPathPatterns("/order/**");
}
@Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
/**
* 时区相关
* @return
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
return builder -> {
builder.indentOutput(true);
builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
};
}
}
测试
// http://localhost:8080/coffee/1
{
"id": 1,
"createTime": "2020-01-20T15:13:07.065+0800",
"updateTime": "2020-01-20T15:13:07.065+0800",
"name": "espresso",
"price": 20.00
}
//控制台输出
2020-01-20 15:13:33.667 INFO 10340 --- [nio-8080-exec-1] g.s.s.w.controller.CoffeeController : Coffee Coffee(super=BaseEntity(id=1, createTime=2020-01-20 15:13:07.065, updateTime=2020-01-20 15:13:07.065), name=espresso, price=CNY 20.00):
2020-01-20 15:13:33.728 INFO 10340 --- [nio-8080-exec-1] g.s.s.w.c.PerformanceInteceptor : /coffee/1;geektime.spring.springbucks.waiter.controller.CoffeeController.getById;200;-;83ms;82ms;1ms