目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

Spring MVC

认识 Spring MVC

DispatcherServlet (核心、也是所有请求的入口)

  • Controller:业务处理逻辑
  • xxxResolver:解析器
1ViewResolver
2HandlerExceptionResolver
3MultipartResolver
  • HandlerMapping:Request 与 Controller 的映射。

Spring MVC 中的常用注解

  • @Controller
  • @RestController :@Controller + @RequestBody。
  • @RequestMapping:此 Controller 要处理哪些请求。
1@GetMapping
2@PostMapping
3@PutMapping
4@DeleteMapping
  • @RequestBody:请求报文体
  • @ResponseBody:响应报文体
  • @ResponseStatus:响应状态码

定义一个简单的 Controller

CoffeeController

 1package geektime.spring.springbucks.waiter.controller;
 2
 3import geektime.spring.springbucks.waiter.model.Coffee;
 4import geektime.spring.springbucks.waiter.service.CoffeeService;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.stereotype.Controller;
 7import org.springframework.web.bind.annotation.GetMapping;
 8import org.springframework.web.bind.annotation.RequestMapping;
 9import org.springframework.web.bind.annotation.ResponseBody;
10
11import java.util.List;
12
13@Controller
14@RequestMapping("/coffee")
15public class CoffeeController {
16    @Autowired
17    private CoffeeService coffeeService;
18
19    @GetMapping("/")
20    @ResponseBody
21    public List<Coffee> getAll() {
22        return coffeeService.getAllCoffee();
23    }
24}

CoffeeOrderController

 1package geektime.spring.springbucks.waiter.controller;
 2
 3import geektime.spring.springbucks.waiter.controller.request.NewOrderRequest;
 4import geektime.spring.springbucks.waiter.model.Coffee;
 5import geektime.spring.springbucks.waiter.model.CoffeeOrder;
 6import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
 7import geektime.spring.springbucks.waiter.service.CoffeeService;
 8import lombok.extern.slf4j.Slf4j;
 9import org.springframework.beans.factory.annotation.Autowired;
10import org.springframework.http.HttpStatus;
11import org.springframework.web.bind.annotation.PostMapping;
12import org.springframework.web.bind.annotation.RequestBody;
13import org.springframework.web.bind.annotation.RequestMapping;
14import org.springframework.web.bind.annotation.ResponseStatus;
15import org.springframework.web.bind.annotation.RestController;
16
17@RestController
18@RequestMapping("/order")
19@Slf4j
20public class CoffeeOrderController {
21    @Autowired
22    private CoffeeOrderService orderService;
23    @Autowired
24    private CoffeeService coffeeService;
25
26    @PostMapping("/")
27    @ResponseStatus(HttpStatus.CREATED)
28    public CoffeeOrder create(@RequestBody NewOrderRequest newOrder) {
29        log.info("Receive new Order {}", newOrder);
30        Coffee[] coffeeList = coffeeService.getCoffeeByName(newOrder.getItems())
31                .toArray(new Coffee[] {});
32        return orderService.createOrder(newOrder.getCustomer(), coffeeList);
33    }
34}

NewOrderRequest

 1package geektime.spring.springbucks.waiter.controller.request;
 2
 3import lombok.Getter;
 4import lombok.Setter;
 5import lombok.ToString;
 6
 7import java.util.List;
 8
 9@Getter
10@Setter
11@ToString
12public class NewOrderRequest {
13    private String customer;
14    private List<String> items;
15}

CoffeeService

 1package geektime.spring.springbucks.waiter.service;
 2
 3import geektime.spring.springbucks.waiter.model.Coffee;
 4import geektime.spring.springbucks.waiter.repository.CoffeeRepository;
 5import lombok.extern.slf4j.Slf4j;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.cache.annotation.CacheConfig;
 8import org.springframework.cache.annotation.Cacheable;
 9import org.springframework.data.domain.Example;
10import org.springframework.data.domain.Sort;
11import org.springframework.stereotype.Service;
12
13import java.util.Arrays;
14import java.util.List;
15import java.util.Map;
16
17@Service
18@Slf4j
19@CacheConfig(cacheNames = "CoffeeCache")
20public class CoffeeService {
21    @Autowired
22    private CoffeeRepository coffeeRepository;
23
24    @Cacheable
25    public List<Coffee> getAllCoffee() {
26        return coffeeRepository.findAll(Sort.by("id"));
27    }
28
29    public List<Coffee> getCoffeeByName(List<String> names) {
30        return coffeeRepository.findByNameInOrderById(names);
31    }
32}

启动类

1@SpringBootApplication
2@EnableJpaRepositories
3@EnableCaching
4public class WaiterServiceApplication {
5	public static void main(String[] args) {
6		SpringApplication.run(WaiterServiceApplication.class, args);
7	}
8}

测试(RestServices)

 1* Found 2 services  
 2* waiter-service  
 3* /coffee/  
 4* /order/
 5
 6/**
 7 * /coffee/  
 8 * @return CoffeeOrder
 9 */
10// http://localhost:8080/coffee/
11
12/**
13 * /order/
14 * @return CoffeeOrder
15 */
16// RequestBody
17{
18  "customer": "demoData",
19  "items": [
20    "demoData"
21  ]
22}
23
24//Response
25{
26  "id": 1,
27  "createTime": "2020-01-19T06:30:04.445+0000",
28  "updateTime": "2020-01-19T06:30:04.445+0000",
29  "customer": "demoData",
30  "items": [],
31  "state": "INIT"
32}

理解 Spring 的应用上下文

Spring Application Context
关于上下文常用的接口及其实现

  • BeanFactory : DefaultListableBeanFactory
  • ApplicationContext: ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、AnnotationConfifigApplicationContext
  • WebApplicationContext:Web 相关上文接口。
    web 相关上文

示例
要增强的 Bean TestBean

 1package geektime.spring.web.context;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.extern.slf4j.Slf4j;
 5
 6@AllArgsConstructor
 7@Slf4j
 8public class TestBean {
 9    private String context;
10
11    public void hello() {
12        log.info("hello " + context);
13    }
14}

切面 FooAspect

 1package geektime.spring.web.foo;
 2
 3import lombok.extern.slf4j.Slf4j;
 4import org.aspectj.lang.annotation.AfterReturning;
 5import org.aspectj.lang.annotation.Aspect;
 6
 7@Aspect
 8@Slf4j
 9public class FooAspect {
10    @AfterReturning("bean(testBean*)")
11    public void printAfter() {
12        log.info("after hello()");
13    }
14}

配置类 FooConfig

 1package geektime.spring.web.foo;
 2
 3import geektime.spring.web.context.TestBean;
 4import org.springframework.context.annotation.Bean;
 5import org.springframework.context.annotation.Configuration;
 6import org.springframework.context.annotation.EnableAspectJAutoProxy;
 7
 8@Configuration
 9@EnableAspectJAutoProxy
10public class FooConfig {
11    @Bean
12    public TestBean testBeanX() {
13        return new TestBean("foo");
14    }
15
16    @Bean
17    public TestBean testBeanY() {
18        return new TestBean("foo");
19    }
20
21    @Bean
22    public FooAspect fooAspect() {
23        return new FooAspect();
24    }
25}

applicationContext.xml

 1<beans xmlns="http://www.springframework.org/schema/beans"
 2       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3       xmlns:aop="http://www.springframework.org/schema/aop"
 4       xsi:schemaLocation="http://www.springframework.org/schema/beans
 5        http://www.springframework.org/schema/beans/spring-beans.xsd
 6        http://www.springframework.org/schema/aop
 7        http://www.springframework.org/schema/aop/spring-aop.xsd">
 8
 9<!--    <aop:aspectj-autoproxy/>-->
10
11    <bean id="testBeanX" class="geektime.spring.web.context.TestBean">
12        <constructor-arg name="context" value="Bar" />
13    </bean>
14
15    <!--<bean id="fooAspect" class="geektime.spring.web.foo.FooAspect" />-->
16</beans>

启动类

 1package geektime.spring.web.foo;
 2
 3import geektime.spring.web.context.TestBean;
 4import org.springframework.context.annotation.Bean;
 5import org.springframework.context.annotation.Configuration;
 6import org.springframework.context.annotation.EnableAspectJAutoProxy;
 7
 8@Configuration
 9@EnableAspectJAutoProxy
10public class FooConfig {
11    @Bean
12    public TestBean testBeanX() {
13        return new TestBean("foo");
14    }
15
16    @Bean
17    public TestBean testBeanY() {
18        return new TestBean("foo");
19    }
20
21    @Bean
22    public FooAspect fooAspect() {
23        return new FooAspect();
24    }
25}

总结
 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。

 1场景一:  
 2父上下文开启 @EnableAspectJAutoProxy 的支持  
 3子上下文未开启 <aop: aspectj-autoproxy />  
 4切面 fooAspect 在 FooConfig.java 定义(父上下文增强)  
 5  
 6输出结果:  
 7testBeanX(foo) 和 testBeanY(foo) 均被增强。  
 8testBeanX(bar) 未被增强。  
 9  
10结论:  
11在父上下文开启了增强,父的 bean 均被增强,而子的 bean 未被增强。
12
13
14场景二:  
15父上下文开启 @EnableAspectJAutoProxy 的支持  
16子上下文开启 <aop: aspectj-autoproxy />  
17切面 fooAspect 在 applicationContext.xml 定义(子上下文增加)  
18  
19输出结果:  
20testBeanX(foo) 和 testBeanY(foo) 未被增强。  
21testBeanX(bar) 被增强。  
22  
23结论:  
24在子上下文开启增强,父的 bean 未被增强,子的 bean 被增强。
25
26
27// 根据场景一和场景二的结果,有结论:
28“各个 context 相互独立,每个 context 的 aop 增强只对本 context 的 bean 生效”。如果想将切面配置成通用的,对父和子上下文的 bean 均支持增强,则:  
291. 切面 fooAspect 定义在父上下文。  
302. 父上下文和子上下文,均要开启 aop 的增加,即 @EnableAspectJAutoProxy 或<aop: aspectj-autoproxy /> 的支持。

Spring MVC 的请求处理流程

一个请求的大致处理流程
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

 1package geektime.spring.springbucks.waiter.controller;
 2
 3import geektime.spring.springbucks.waiter.controller.request.NewOrderRequest;
 4import geektime.spring.springbucks.waiter.model.Coffee;
 5import geektime.spring.springbucks.waiter.model.CoffeeOrder;
 6import geektime.spring.springbucks.waiter.service.CoffeeOrderService;
 7import geektime.spring.springbucks.waiter.service.CoffeeService;
 8import lombok.extern.slf4j.Slf4j;
 9import org.springframework.beans.factory.annotation.Autowired;
10import org.springframework.http.HttpStatus;
11import org.springframework.http.MediaType;
12import org.springframework.web.bind.annotation.GetMapping;
13import org.springframework.web.bind.annotation.PathVariable;
14import org.springframework.web.bind.annotation.PostMapping;
15import org.springframework.web.bind.annotation.RequestBody;
16import org.springframework.web.bind.annotation.RequestMapping;
17import org.springframework.web.bind.annotation.ResponseStatus;
18import org.springframework.web.bind.annotation.RestController;
19
20@RestController
21@RequestMapping("/order")
22@Slf4j
23public class CoffeeOrderController {
24    @Autowired
25    private CoffeeOrderService orderService;
26    @Autowired
27    private CoffeeService coffeeService;
28
29    @GetMapping("/{id}")
30    public CoffeeOrder getOrder(@PathVariable("id") Long id) {
31        return orderService.get(id);
32    }
33
34    @PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_VALUE,
35            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
36    @ResponseStatus(HttpStatus.CREATED)
37    public CoffeeOrder create(@RequestBody NewOrderRequest newOrder) {
38        log.info("Receive new Order {}", newOrder);
39        Coffee[] coffeeList = coffeeService.getCoffeeByName(newOrder.getItems())
40                .toArray(new Coffee[] {});
41        return orderService.createOrder(newOrder.getCustomer(), coffeeList);
42    }
43}

CoffeeController

 1package geektime.spring.springbucks.waiter.controller;
 2
 3import geektime.spring.springbucks.waiter.model.Coffee;
 4import geektime.spring.springbucks.waiter.service.CoffeeService;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.http.MediaType;
 7import org.springframework.stereotype.Controller;
 8import org.springframework.web.bind.annotation.GetMapping;
 9import org.springframework.web.bind.annotation.PathVariable;
10import org.springframework.web.bind.annotation.RequestMapping;
11import org.springframework.web.bind.annotation.RequestMethod;
12import org.springframework.web.bind.annotation.RequestParam;
13import org.springframework.web.bind.annotation.ResponseBody;
14
15import java.util.List;
16
17@Controller
18@RequestMapping("/coffee")
19public class CoffeeController {
20    @Autowired
21    private CoffeeService coffeeService;
22
23    @GetMapping(path = "/", params = "!name")
24    @ResponseBody
25    public List<Coffee> getAll() {
26        return coffeeService.getAllCoffee();
27    }
28
29    @RequestMapping(path = "/{id}", method = RequestMethod.GET,
30            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
31    @ResponseBody
32    public Coffee getById(@PathVariable Long id) {
33        Coffee coffee = coffeeService.getCoffee(id);
34        return coffee;
35    }
36
37    @GetMapping(path = "/", params = "name")
38    @ResponseBody
39    public Coffee getByName(@RequestParam String name) {
40        return coffeeService.getCoffee(name);
41    }
42}

Spring MVC 支持的视图

支持视图列表

JSON 视图

  • Jackson-based JSON / XML
  • Thymeleaf & FreeMarker

配置 MessageConverter

  • 通过 WebMvcConfigurer 的 configureMessageConverters()
1 Spring Boot ⾃动查找 HttpMessageConverters 进⾏注册

Spring Boot 对 Jackson 的支持

  • JacksonAutoConfiguration
1 Spring Boot 通过 @JsonComponent 注册 JSON 序列化组件
2 Jackson2ObjectMapperBuilderCustomizer  

JacksonHttpMessageConvertersConfiguration

  • 增加 jackson-dataformat-xml 以⽀持 XML 序列化

示例
依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.springbucks</groupId>
12	<artifactId>waiter-service</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>waiter-service</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-cache</artifactId>
25		</dependency>
26		<dependency>
27			<groupId>org.springframework.boot</groupId>
28			<artifactId>spring-boot-starter-data-jpa</artifactId>
29		</dependency>
30		<dependency>
31			<groupId>org.springframework.boot</groupId>
32			<artifactId>spring-boot-starter-web</artifactId>
33		</dependency>
34
35		<dependency>
36			<groupId>org.joda</groupId>
37			<artifactId>joda-money</artifactId>
38			<version>1.0.1</version>
39		</dependency>
40		<dependency>
41			<groupId>org.jadira.usertype</groupId>
42			<artifactId>usertype.core</artifactId>
43			<version>6.0.1.GA</version>
44		</dependency>
45		<!-- 增加Jackson的Hibernate类型支持 -->
46		<dependency>
47			<groupId>com.fasterxml.jackson.datatype</groupId>
48			<artifactId>jackson-datatype-hibernate5</artifactId>
49			<version>2.9.8</version>
50		</dependency>
51		<!-- 增加Jackson XML支持 -->
52		<dependency>
53			<groupId>com.fasterxml.jackson.dataformat</groupId>
54			<artifactId>jackson-dataformat-xml</artifactId>
55			<version>2.9.0</version>
56		</dependency>
57
58		<dependency>
59			<groupId>org.apache.commons</groupId>
60			<artifactId>commons-lang3</artifactId>
61		</dependency>
62
63		<dependency>
64			<groupId>com.h2database</groupId>
65			<artifactId>h2</artifactId>
66			<scope>runtime</scope>
67		</dependency>
68		<dependency>
69			<groupId>org.projectlombok</groupId>
70			<artifactId>lombok</artifactId>
71			<optional>true</optional>
72		</dependency>
73		<dependency>
74			<groupId>org.springframework.boot</groupId>
75			<artifactId>spring-boot-starter-test</artifactId>
76			<scope>test</scope>
77		</dependency>
78	</dependencies>
79
80	<build>
81		<plugins>
82			<plugin>
83				<groupId>org.springframework.boot</groupId>
84				<artifactId>spring-boot-maven-plugin</artifactId>
85			</plugin>
86		</plugins>
87	</build>
88</project>

BaseEntity

 1package geektime.spring.springbucks.waiter.model;
 2
 3import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 4import lombok.AllArgsConstructor;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import org.hibernate.annotations.CreationTimestamp;
 8import org.hibernate.annotations.UpdateTimestamp;
 9
10import javax.persistence.Column;
11import javax.persistence.GeneratedValue;
12import javax.persistence.GenerationType;
13import javax.persistence.Id;
14import javax.persistence.MappedSuperclass;
15import java.io.Serializable;
16import java.util.Date;
17
18@MappedSuperclass
19@Data
20@NoArgsConstructor
21@AllArgsConstructor
22// 增加了jackson-datatype-hibernate5就不需要这个Ignore了
23//@JsonIgnoreProperties(value = {"hibernateLazyInitializer"})
24public class BaseEntity implements Serializable {
25    @Id
26    @GeneratedValue(strategy = GenerationType.IDENTITY)
27    private Long id;
28    @Column(updatable = false)
29    @CreationTimestamp
30    private Date createTime;
31    @UpdateTimestamp
32    private Date updateTime;
33}

MoneySerializer

 1package geektime.spring.springbucks.waiter.support;
 2
 3import com.fasterxml.jackson.core.JsonGenerator;
 4import com.fasterxml.jackson.databind.SerializerProvider;
 5import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 6import org.joda.money.Money;
 7import org.springframework.boot.jackson.JsonComponent;
 8
 9import java.io.IOException;
10
11@JsonComponent
12public class MoneySerializer extends StdSerializer<Money> {
13    protected MoneySerializer() {
14        super(Money.class);
15    }
16
17    @Override
18    public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
19        jsonGenerator.writeNumber(money.getAmount());
20    }
21}

MoneyDeserializer

 1package geektime.spring.springbucks.waiter.support;
 2
 3import com.fasterxml.jackson.core.JsonParser;
 4import com.fasterxml.jackson.core.JsonProcessingException;
 5import com.fasterxml.jackson.databind.DeserializationContext;
 6import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 7import org.joda.money.CurrencyUnit;
 8import org.joda.money.Money;
 9import org.springframework.boot.jackson.JsonComponent;
10
11import java.io.IOException;
12
13@JsonComponent
14public class MoneyDeserializer extends StdDeserializer<Money> {
15    protected MoneyDeserializer() {
16        super(Money.class);
17    }
18
19    @Override
20    public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
21        return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
22    }
23}

CoffeeController

  1package geektime.spring.springbucks.waiter.controller;
  2
  3import geektime.spring.springbucks.waiter.controller.request.NewCoffeeRequest;
  4import geektime.spring.springbucks.waiter.model.Coffee;
  5import geektime.spring.springbucks.waiter.service.CoffeeService;
  6import lombok.extern.slf4j.Slf4j;
  7import org.apache.commons.lang3.StringUtils;
  8import org.apache.commons.lang3.math.NumberUtils;
  9import org.apache.tomcat.util.http.fileupload.IOUtils;
 10import org.joda.money.CurrencyUnit;
 11import org.joda.money.Money;
 12import org.springframework.beans.factory.annotation.Autowired;
 13import org.springframework.http.HttpStatus;
 14import org.springframework.http.MediaType;
 15import org.springframework.stereotype.Controller;
 16import org.springframework.validation.BindingResult;
 17import org.springframework.web.bind.annotation.GetMapping;
 18import org.springframework.web.bind.annotation.PathVariable;
 19import org.springframework.web.bind.annotation.PostMapping;
 20import org.springframework.web.bind.annotation.RequestBody;
 21import org.springframework.web.bind.annotation.RequestMapping;
 22import org.springframework.web.bind.annotation.RequestMethod;
 23import org.springframework.web.bind.annotation.RequestParam;
 24import org.springframework.web.bind.annotation.ResponseBody;
 25import org.springframework.web.bind.annotation.ResponseStatus;
 26import org.springframework.web.multipart.MultipartFile;
 27
 28import javax.validation.Valid;
 29import java.io.BufferedInputStream;
 30import java.io.BufferedReader;
 31import java.io.FileReader;
 32import java.io.IOError;
 33import java.io.IOException;
 34import java.io.InputStreamReader;
 35import java.util.ArrayList;
 36import java.util.List;
 37
 38@Controller
 39@RequestMapping("/coffee")
 40@Slf4j
 41public class CoffeeController {
 42    @Autowired
 43    private CoffeeService coffeeService;
 44
 45//    @PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 46//    @ResponseBody
 47//    @ResponseStatus(HttpStatus.CREATED)
 48//    public Coffee addCoffee(@Valid NewCoffeeRequest newCoffee,
 49//                            BindingResult result) {
 50//        if (result.hasErrors()) {
 51//            // 这里先简单处理一下,后续讲到异常处理时会改
 52//            log.warn("Binding Errors: {}", result);
 53//            return null;
 54//        }
 55//        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 56//    }
 57
 58    @PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 59    @ResponseBody
 60    @ResponseStatus(HttpStatus.CREATED)
 61    public Coffee addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee) {
 62        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 63    }
 64
 65    @PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
 66    @ResponseBody
 67    @ResponseStatus(HttpStatus.CREATED)
 68    public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) {
 69        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 70    }
 71
 72    @PostMapping(path = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
 73    @ResponseBody
 74    @ResponseStatus(HttpStatus.CREATED)
 75    public List<Coffee> batchAddCoffee(@RequestParam("file") MultipartFile file) {
 76        List<Coffee> coffees = new ArrayList<>();
 77        if (!file.isEmpty()) {
 78            BufferedReader reader = null;
 79            try {
 80                reader = new BufferedReader(
 81                        new InputStreamReader(file.getInputStream()));
 82                String str;
 83                while ((str = reader.readLine()) != null) {
 84                    String[] arr = StringUtils.split(str, " ");
 85                    if (arr != null && arr.length == 2) {
 86                        coffees.add(coffeeService.saveCoffee(arr[0],
 87                                Money.of(CurrencyUnit.of("CNY"),
 88                                        NumberUtils.createBigDecimal(arr[1]))));
 89                    }
 90                }
 91            } catch (IOException e) {
 92                log.error("exception", e);
 93            } finally {
 94                IOUtils.closeQuietly(reader);
 95            }
 96        }
 97        return coffees;
 98    }
 99
100    @GetMapping(path = "/", params = "!name")
101    @ResponseBody
102    public List<Coffee> getAll() {
103        return coffeeService.getAllCoffee();
104    }
105
106    @RequestMapping(path = "/{id}", method = RequestMethod.GET)
107    @ResponseBody
108    public Coffee getById(@PathVariable Long id) {
109        Coffee coffee = coffeeService.getCoffee(id);
110        log.info("Coffee {}:", coffee);
111        return coffee;
112    }
113
114    @GetMapping(path = "/", params = "name")
115    @ResponseBody
116    public Coffee getByName(@RequestParam String name) {
117        return coffeeService.getCoffee(name);
118    }
119}

启动类

 1package geektime.spring.springbucks.waiter;
 2
 3import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
 4import org.springframework.boot.SpringApplication;
 5import org.springframework.boot.autoconfigure.SpringBootApplication;
 6import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 7import org.springframework.cache.annotation.EnableCaching;
 8import org.springframework.context.annotation.Bean;
 9import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
10
11@SpringBootApplication
12@EnableJpaRepositories
13@EnableCaching
14public class WaiterServiceApplication {
15
16	public static void main(String[] args) {
17		SpringApplication.run(WaiterServiceApplication.class, args);
18	}
19
20	@Bean
21	public Hibernate5Module hibernate5Module() {
22		return new Hibernate5Module();
23	}
24
25	/**
26	 * 开启缩进显示:Terminal 命令使用的时候
27	 * @return
28	 */
29	@Bean
30	public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
31		return builder -> builder.indentOutput(true);
32	}
33}

Spring MVC 的异常解析

HandlerExceptionResolver(核心接口):常见的实现类如下

  • SimpleMappingExceptionResolver
  • DefaultHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • ExceptionHandlerExceptionResolver

异常处理方法
@* ExceptionHandler:

添加位置

  • @Controller / @RestController
  • @ControllerAdvice / @RestControllerAdvice

示例
FormValidationException 自定义异常

 1package geektime.spring.springbucks.waiter.controller.exception;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Getter;
 5import org.springframework.http.HttpStatus;
 6import org.springframework.validation.BindingResult;
 7import org.springframework.web.bind.annotation.ResponseStatus;
 8
 9@ResponseStatus(HttpStatus.BAD_REQUEST)
10@Getter
11@AllArgsConstructor
12public class FormValidationException extends RuntimeException {
13    private BindingResult result;
14}

GlobalControllerAdvice 全局异常处理

 1package geektime.spring.springbucks.waiter.controller;
 2
 3import geektime.spring.springbucks.waiter.controller.exception.FormValidationException;
 4import org.springframework.http.HttpStatus;
 5import org.springframework.web.bind.annotation.ExceptionHandler;
 6import org.springframework.web.bind.annotation.ResponseStatus;
 7import org.springframework.web.bind.annotation.RestControllerAdvice;
 8
 9import javax.validation.ValidationException;
10import java.util.HashMap;
11import java.util.Map;
12
13@RestControllerAdvice
14public class GlobalControllerAdvice {
15    @ExceptionHandler(ValidationException.class)//专门用于处理ValidationException异常
16    @ResponseStatus(HttpStatus.BAD_REQUEST)
17    public Map<String, String> validationExceptionHandler(ValidationException exception) {
18        Map<String, String> map = new HashMap<>();
19        map.put("message", exception.getMessage());
20        return map;
21    }
22}

NewCoffeeRequest

 1package geektime.spring.springbucks.waiter.controller.request;
 2
 3import lombok.Getter;
 4import lombok.Setter;
 5import lombok.ToString;
 6import org.joda.money.Money;
 7
 8import javax.validation.constraints.NotEmpty;
 9import javax.validation.constraints.NotNull;
10
11@Getter
12@Setter
13@ToString
14public class NewCoffeeRequest {
15    @NotEmpty
16    private String name;
17    @NotNull
18    private Money price;
19}

CoffeeController

  1package geektime.spring.springbucks.waiter.controller;
  2
  3import geektime.spring.springbucks.waiter.controller.exception.FormValidationException;
  4import geektime.spring.springbucks.waiter.controller.request.NewCoffeeRequest;
  5import geektime.spring.springbucks.waiter.model.Coffee;
  6import geektime.spring.springbucks.waiter.service.CoffeeService;
  7import lombok.extern.slf4j.Slf4j;
  8import org.apache.commons.lang3.StringUtils;
  9import org.apache.commons.lang3.math.NumberUtils;
 10import org.apache.tomcat.util.http.fileupload.IOUtils;
 11import org.joda.money.CurrencyUnit;
 12import org.joda.money.Money;
 13import org.springframework.beans.factory.annotation.Autowired;
 14import org.springframework.http.HttpStatus;
 15import org.springframework.http.MediaType;
 16import org.springframework.stereotype.Controller;
 17import org.springframework.validation.BindingResult;
 18import org.springframework.web.bind.annotation.GetMapping;
 19import org.springframework.web.bind.annotation.PathVariable;
 20import org.springframework.web.bind.annotation.PostMapping;
 21import org.springframework.web.bind.annotation.RequestBody;
 22import org.springframework.web.bind.annotation.RequestMapping;
 23import org.springframework.web.bind.annotation.RequestMethod;
 24import org.springframework.web.bind.annotation.RequestParam;
 25import org.springframework.web.bind.annotation.ResponseBody;
 26import org.springframework.web.bind.annotation.ResponseStatus;
 27import org.springframework.web.multipart.MultipartFile;
 28
 29import javax.validation.Valid;
 30import javax.validation.ValidationException;
 31import java.io.BufferedReader;
 32import java.io.IOException;
 33import java.io.InputStreamReader;
 34import java.util.ArrayList;
 35import java.util.List;
 36
 37@Controller
 38@RequestMapping("/coffee")
 39@Slf4j
 40public class CoffeeController {
 41    @Autowired
 42    private CoffeeService coffeeService;
 43
 44    @PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 45    @ResponseBody
 46    @ResponseStatus(HttpStatus.CREATED)
 47    public Coffee addCoffee(@Valid NewCoffeeRequest newCoffee,
 48                            BindingResult result) {
 49        if (result.hasErrors()) {
 50            log.warn("Binding Errors: {}", result);
 51            throw new FormValidationException(result);
 52        }
 53        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 54    }
 55
 56    @PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
 57    @ResponseBody
 58    @ResponseStatus(HttpStatus.CREATED)
 59    public Coffee addJsonCoffee(@Valid @RequestBody NewCoffeeRequest newCoffee,
 60                                BindingResult result) {
 61        if (result.hasErrors()) {
 62            log.warn("Binding Errors: {}", result);
 63            throw new ValidationException(result.toString());
 64        }
 65        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 66    }
 67
 68//    @PostMapping(path = "/", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 69//    @ResponseBody
 70//    @ResponseStatus(HttpStatus.CREATED)
 71//    public Coffee addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee) {
 72//        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 73//    }
 74
 75//    @PostMapping(path = "/", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
 76//    @ResponseBody
 77//    @ResponseStatus(HttpStatus.CREATED)
 78//    public Coffee addJsonCoffeeWithoutBindingResult(@Valid @RequestBody NewCoffeeRequest newCoffee) {
 79//        return coffeeService.saveCoffee(newCoffee.getName(), newCoffee.getPrice());
 80//    }
 81
 82    @PostMapping(path = "/", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
 83    @ResponseBody
 84    @ResponseStatus(HttpStatus.CREATED)
 85    public List<Coffee> batchAddCoffee(@RequestParam("file") MultipartFile file) {
 86        List<Coffee> coffees = new ArrayList<>();
 87        if (!file.isEmpty()) {
 88            BufferedReader reader = null;
 89            try {
 90                reader = new BufferedReader(
 91                        new InputStreamReader(file.getInputStream()));
 92                String str;
 93                while ((str = reader.readLine()) != null) {
 94                    String[] arr = StringUtils.split(str, " ");
 95                    if (arr != null && arr.length == 2) {
 96                        coffees.add(coffeeService.saveCoffee(arr[0],
 97                                Money.of(CurrencyUnit.of("CNY"),
 98                                        NumberUtils.createBigDecimal(arr[1]))));
 99                    }
100                }
101            } catch (IOException e) {
102                log.error("exception", e);
103            } finally {
104                IOUtils.closeQuietly(reader);
105            }
106        }
107        return coffees;
108    }
109
110    @GetMapping(path = "/", params = "!name")
111    @ResponseBody
112    public List<Coffee> getAll() {
113        return coffeeService.getAllCoffee();
114    }
115
116    @RequestMapping(path = "/{id}", method = RequestMethod.GET)
117    @ResponseBody
118    public Coffee getById(@PathVariable Long id) {
119        Coffee coffee = coffeeService.getCoffee(id);
120        log.info("Coffee {}:", coffee);
121        return coffee;
122    }
123
124    @GetMapping(path = "/", params = "name")
125    @ResponseBody
126    public Coffee getByName(@RequestParam String name) {
127        return coffeeService.getCoffee(name);
128    }
129}

Spring MVC 的拦截器

HandlerInteceptor(核心接口):实现类如下

  • boolean preHandle() : 权限验证等……
  • void postHandle()
  • void afterCompletion()

针对 @ResponseBody 和 ResponseEntity 的情况

  • ResponseBodyAdvice

针对异步请求的接口

  • AsyncHandlerInterceptor:void afterConcurrentHandlingStarted()

拦截器的配置方式

  • 常规方法:WebMvcConfigurer.addInterceptors()

Spring Boot 中的配置

  • 创建⼀个带 @Configuration 的 WebMvcConfigurer 配置类
  • 不能带 @EnableWebMvc(想彻底自己控制 MVC 配置除外)

示例
PerformanceInteceptor

 1package geektime.spring.springbucks.waiter.controller;
 2
 3import lombok.extern.slf4j.Slf4j;
 4import org.springframework.util.StopWatch;
 5import org.springframework.web.method.HandlerMethod;
 6import org.springframework.web.servlet.HandlerInterceptor;
 7import org.springframework.web.servlet.ModelAndView;
 8
 9import javax.servlet.http.HttpServletRequest;
10import javax.servlet.http.HttpServletResponse;
11
12@Slf4j
13public class PerformanceInteceptor implements HandlerInterceptor {
14    private ThreadLocal<StopWatch> stopWatch = new ThreadLocal<>();
15
16    @Override
17    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
18        StopWatch sw = new StopWatch();
19        stopWatch.set(sw);
20        sw.start();
21        return true;
22    }
23
24    @Override
25    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
26        stopWatch.get().stop();
27        stopWatch.get().start();
28    }
29
30    @Override
31    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
32        StopWatch sw = stopWatch.get();
33        sw.stop();
34        String method = handler.getClass().getSimpleName();
35        if (handler instanceof HandlerMethod) {
36            String beanType = ((HandlerMethod) handler).getBeanType().getName();
37            String methodName = ((HandlerMethod) handler).getMethod().getName();
38            method = beanType + "." + methodName;
39        }
40        log.info("{};{};{};{};{}ms;{}ms;{}ms", request.getRequestURI(), method,
41                response.getStatus(), ex == null ? "-" : ex.getClass().getSimpleName(),
42                sw.getTotalTimeMillis(), sw.getTotalTimeMillis() - sw.getLastTaskTimeMillis(),
43                sw.getLastTaskTimeMillis());
44        stopWatch.remove();
45    }
46}
47

WaiterServiceApplication 启动类

 1package geektime.spring.springbucks.waiter;
 2
 3import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
 4import geektime.spring.springbucks.waiter.controller.PerformanceInteceptor;
 5import org.springframework.boot.SpringApplication;
 6import org.springframework.boot.autoconfigure.SpringBootApplication;
 7import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 8import org.springframework.cache.annotation.EnableCaching;
 9import org.springframework.context.annotation.Bean;
10import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
11import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
12import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
13
14import java.util.TimeZone;
15
16@SpringBootApplication//@SpringBootConfiguration 相当于 @Configuration
17@EnableJpaRepositories
18@EnableCaching
19public class WaiterServiceApplication implements WebMvcConfigurer {
20
21	public static void main(String[] args) {
22		SpringApplication.run(WaiterServiceApplication.class, args);
23	}
24
25	/**
26	 * 添加拦截器
27	 * @param registry
28	 */
29	@Override
30	public void addInterceptors(InterceptorRegistry registry) {
31		registry.addInterceptor(new PerformanceInteceptor())
32				.addPathPatterns("/coffee/**").addPathPatterns("/order/**");
33	}
34
35	@Bean
36	public Hibernate5Module hibernate5Module() {
37		return new Hibernate5Module();
38	}
39
40	/**
41	 * 时区相关
42	 * @return
43	 */
44	@Bean
45	public Jackson2ObjectMapperBuilderCustomizer jacksonBuilderCustomizer() {
46		return builder -> {
47			builder.indentOutput(true);
48			builder.timeZone(TimeZone.getTimeZone("Asia/Shanghai"));
49		};
50	}
51}

测试

 1// http://localhost:8080/coffee/1
 2{
 3  "id": 1,
 4  "createTime": "2020-01-20T15:13:07.065+0800",
 5  "updateTime": "2020-01-20T15:13:07.065+0800",
 6  "name": "espresso",
 7  "price": 20.00
 8}
 9
10//控制台输出
112020-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):
122020-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

作者:Soulboy