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 的应用上下文
关于上下文常用的接口及其实现
- BeanFactory :
DefaultListableBeanFactory
- ApplicationContext:
ClassPathXmlApplicationContext、FileSystemXmlApplicationContext、AnnotationConfifigApplicationContext
- WebApplicationContext: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 的请求处理流程
一个请求的大致处理流程
- 绑定 ⼀些 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