diff --git a/lab-22/pom.xml b/lab-22/pom.xml
new file mode 100644
index 000000000..aa9ce8700
--- /dev/null
+++ b/lab-22/pom.xml
@@ -0,0 +1,15 @@
+
+
+
+ labs-parent
+ cn.iocoder.springboot.labs
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ lab-22
+
+
+
diff --git a/lab-23/lab-springmvc-23-01/pom.xml b/lab-23/lab-springmvc-23-01/pom.xml
new file mode 100644
index 000000000..0cba57362
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/pom.xml
@@ -0,0 +1,31 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.3.RELEASE
+
+
+ 4.0.0
+
+ lab-springmvc-23-01
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
diff --git a/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/Application.java b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/Application.java
new file mode 100644
index 000000000..212310ef2
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/Application.java
@@ -0,0 +1,13 @@
+package cn.iocoder.springboot.lab21.springmvc;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController.java b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController.java
new file mode 100644
index 000000000..0a06b5469
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController.java
@@ -0,0 +1,91 @@
+package cn.iocoder.springboot.lab21.springmvc.controller;
+
+import cn.iocoder.springboot.lab21.springmvc.dto.UserAddDTO;
+import cn.iocoder.springboot.lab21.springmvc.dto.UserUpdateDTO;
+import cn.iocoder.springboot.lab21.springmvc.vo.UserVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 用户 Controller
+ */
+@RestController
+@RequestMapping("/users")
+public class UserController {
+
+ /**
+ * 查询用户列表
+ *
+ * @return 用户列表
+ */
+ @GetMapping("")
+ public List list() {
+ // 查询列表
+ List result = new ArrayList<>();
+ result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
+ result.add(new UserVO().setId(2).setUsername("woshiyutou"));
+ result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
+ // 返回列表
+ return result;
+ }
+
+ /**
+ * 获得指定用户编号的用户
+ *
+ * @param id 用户编号
+ * @return 用户
+ */
+ @GetMapping("/{id}")
+ public UserVO get(@PathVariable("id") Integer id) {
+ // 查询并返回用户
+ return new UserVO().setId(id).setUsername("username:" + id);
+ }
+
+ /**
+ * 添加用户
+ *
+ * @param addDTO 添加用户信息 DTO
+ * @return 添加成功的用户编号
+ */
+ @PostMapping("")
+ public Integer add(UserAddDTO addDTO) {
+ // 插入用户记录,返回编号
+ Integer returnId = 1;
+ // 返回用户编号
+ return returnId;
+ }
+
+ /**
+ * 更新指定用户编号的用户
+ *
+ * @param id 用户编号
+ * @param updateDTO 更新用户信息 DTO
+ * @return 是否修改成功
+ */
+ @PutMapping("/{id}")
+ public Boolean update(@PathVariable("id") Integer id, UserUpdateDTO updateDTO) {
+ // 将 id 设置到 updateDTO 中
+ updateDTO.setId(id);
+ // 更新用户记录
+ Boolean success = true;
+ // 返回更新是否成功
+ return success;
+ }
+
+ /**
+ * 删除指定用户编号的用户
+ *
+ * @param id 用户编号
+ * @return 是否删除成功
+ */
+ @DeleteMapping("/{id}")
+ public Boolean delete(@PathVariable("id") Integer id) {
+ // 删除用户记录
+ Boolean success = false;
+ // 返回是否更新成功
+ return success;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController2.java b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController2.java
new file mode 100644
index 000000000..ab2a89bc8
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController2.java
@@ -0,0 +1,89 @@
+package cn.iocoder.springboot.lab21.springmvc.controller;
+
+import cn.iocoder.springboot.lab21.springmvc.dto.UserAddDTO;
+import cn.iocoder.springboot.lab21.springmvc.dto.UserUpdateDTO;
+import cn.iocoder.springboot.lab21.springmvc.vo.UserVO;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * 用户2 Controller
+ */
+@RestController
+@RequestMapping("/users2")
+public class UserController2 {
+
+ /**
+ * 查询用户列表
+ *
+ * @return 用户列表
+ */
+ @GetMapping("/list") // URL 修改成 /list
+ public List list() {
+ // 查询列表
+ List result = new ArrayList<>();
+ result.add(new UserVO().setId(1).setUsername("yudaoyuanma"));
+ result.add(new UserVO().setId(2).setUsername("woshiyutou"));
+ result.add(new UserVO().setId(3).setUsername("chifanshuijiao"));
+ // 返回列表
+ return result;
+ }
+
+ /**
+ * 获得指定用户编号的用户
+ *
+ * @param id 用户编号
+ * @return 用户
+ */
+ @GetMapping("/get") // URL 修改成 /get
+ public UserVO get(@RequestParam("id") Integer id) {
+ // 查询并返回用户
+ return new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
+ }
+
+ /**
+ * 添加用户
+ *
+ * @param addDTO 添加用户信息 DTO
+ * @return 添加成功的用户编号
+ */
+ @PostMapping("add") // URL 修改成 /add
+ public Integer add(UserAddDTO addDTO) {
+ // 插入用户记录,返回编号
+ Integer returnId = UUID.randomUUID().hashCode();
+ // 返回用户编号
+ return returnId;
+ }
+
+ /**
+ * 更新指定用户编号的用户
+ *
+ * @param updateDTO 更新用户信息 DTO
+ * @return 是否修改成功
+ */
+ @PostMapping("/update") // URL 修改成 /update ,RequestMethod 改成 POST
+ public Boolean update(UserUpdateDTO updateDTO) {
+ // 更新用户记录
+ Boolean success = true;
+ // 返回更新是否成功
+ return success;
+ }
+
+ /**
+ * 删除指定用户编号的用户
+ *
+ * @param id 用户编号
+ * @return 是否删除成功
+ */
+ @DeleteMapping("/delete") // URL 修改成 /delete ,RequestMethod 改成 DELETE
+ public Boolean delete(@RequestParam("id") Integer id) {
+ // 删除用户记录
+ Boolean success = false;
+ // 返回是否更新成功
+ return success;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/dto/UserAddDTO.java b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/dto/UserAddDTO.java
new file mode 100644
index 000000000..c70ffd434
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/dto/UserAddDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.springboot.lab21.springmvc.dto;
+
+/**
+ * 用户添加 DTO
+ */
+public class UserAddDTO {
+
+ /**
+ * 账号
+ */
+ private String username;
+ /**
+ * 密码
+ */
+ private String password;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public UserAddDTO setUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public UserAddDTO setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/dto/UserUpdateDTO.java b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/dto/UserUpdateDTO.java
new file mode 100644
index 000000000..1995d4f41
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/dto/UserUpdateDTO.java
@@ -0,0 +1,45 @@
+package cn.iocoder.springboot.lab21.springmvc.dto;
+
+public class UserUpdateDTO {
+
+ /**
+ * 编号
+ */
+ private Integer id;
+ /**
+ * 账号
+ */
+ private String username;
+ /**
+ * 密码
+ */
+ private String password;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public UserUpdateDTO setId(Integer id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public UserUpdateDTO setUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public UserUpdateDTO setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/vo/UserVO.java b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/vo/UserVO.java
new file mode 100644
index 000000000..c5033fcb5
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/main/java/cn/iocoder/springboot/lab21/springmvc/vo/UserVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.springboot.lab21.springmvc.vo;
+
+/**
+ * 用户 VO
+ */
+public class UserVO {
+
+ /**
+ * 编号
+ */
+ private Integer id;
+ /**
+ * 账号
+ */
+ private String username;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public UserVO setId(Integer id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public UserVO setUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/test/java/cn/iocoder/springboot/lab21/springmvc/controller/UserControllerTest.java b/lab-23/lab-springmvc-23-01/src/test/java/cn/iocoder/springboot/lab21/springmvc/controller/UserControllerTest.java
new file mode 100644
index 000000000..f45d52270
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/test/java/cn/iocoder/springboot/lab21/springmvc/controller/UserControllerTest.java
@@ -0,0 +1,96 @@
+package cn.iocoder.springboot.lab21.springmvc.controller;
+
+import cn.iocoder.springboot.lab21.springmvc.Application;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = Application.class)
+public class UserControllerTest {
+
+ private MockMvc mvc;
+
+ @Autowired
+ private UserController userController;
+
+ @Before
+ public void setUp() {
+ mvc = MockMvcBuilders.standaloneSetup(userController).build();
+// mvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
+ }
+
+ @Test
+ public void testList() throws Exception {
+ // 查询用户列表
+ ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get("/users"));
+ // 校验结果
+ resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
+ resultActions.andExpect(MockMvcResultMatchers.content().json("[\n" +
+ " {\n" +
+ " \"id\": 1,\n" +
+ " \"username\": \"yudaoyuanma\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": 2,\n" +
+ " \"username\": \"woshiyutou\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": 3,\n" +
+ " \"username\": \"chifanshuijiao\"\n" +
+ " }\n" +
+ "]")); // 响应结果
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ // 获得指定用户编号的用户
+ ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get("/users/1"));
+ // 校验结果
+ resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
+ resultActions.andExpect(MockMvcResultMatchers.content().json("{\n" +
+ "\"id\": 1,\n" +
+ "\"username\": \"username:1\"\n" +
+ "}")); // 响应结果
+ }
+
+ @Test
+ public void testAdd() throws Exception {
+ // 获得指定用户编号的用户
+ ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.post("/users")
+ .param("username", "yudaoyuanma")
+ .param("passowrd", "nicai"));
+ // 校验结果
+ resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
+ resultActions.andExpect(MockMvcResultMatchers.content().string("1")); // 响应结果
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ // 获得指定用户编号的用户
+ ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.put("/users/1")
+ .param("username", "yudaoyuanma")
+ .param("passowrd", "nicai"));
+ // 校验结果
+ resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
+ resultActions.andExpect(MockMvcResultMatchers.content().string("true")); // 响应结果
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ // 获得指定用户编号的用户
+ ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.delete("/users/1"));
+ // 校验结果
+ resultActions.andExpect(MockMvcResultMatchers.status().isOk()); // 响应状态码 200
+ resultActions.andExpect(MockMvcResultMatchers.content().string("false")); // 响应结果
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-01/src/test/java/cn/iocoder/springboot/lab21/springmvc/package-info.java b/lab-23/lab-springmvc-23-01/src/test/java/cn/iocoder/springboot/lab21/springmvc/package-info.java
new file mode 100644
index 000000000..4a93fffd9
--- /dev/null
+++ b/lab-23/lab-springmvc-23-01/src/test/java/cn/iocoder/springboot/lab21/springmvc/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.springboot.lab21.springmvc;
diff --git a/lab-23/lab-springmvc-23-02/pom.xml b/lab-23/lab-springmvc-23-02/pom.xml
new file mode 100644
index 000000000..f222c0f93
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/pom.xml
@@ -0,0 +1,31 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.1.3.RELEASE
+
+
+ 4.0.0
+
+ lab-springmvc-23-02
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/Application.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/Application.java
new file mode 100644
index 000000000..212310ef2
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/Application.java
@@ -0,0 +1,13 @@
+package cn.iocoder.springboot.lab21.springmvc;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/constants/ServiceExceptionEnum.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/constants/ServiceExceptionEnum.java
new file mode 100644
index 000000000..4e7b036c4
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/constants/ServiceExceptionEnum.java
@@ -0,0 +1,52 @@
+package cn.iocoder.springboot.lab21.springmvc.constants;
+
+/**
+ * 业务异常枚举
+ */
+public enum ServiceExceptionEnum {
+ // ========== 系统级别 ==========
+ SUCCESS(0, "成功"),
+ SYS_ERROR(2001001000, "服务端发生异常"),
+ MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
+
+ // ========== 用户模块 ==========
+ USER_NOT_FOUND(1001002000, "用户不存在"),
+
+ // ========== 订单模块 ==========
+
+ // ========== 商品模块 ==========
+ ;
+
+ /**
+ * 错误码
+ */
+ private int code;
+ /**
+ * 错误提示
+ */
+ private String message;
+
+ ServiceExceptionEnum(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public ServiceExceptionEnum setCode(int code) {
+ this.code = code;
+ return this;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public ServiceExceptionEnum setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController.java
new file mode 100644
index 000000000..4567a1e43
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller/UserController.java
@@ -0,0 +1,67 @@
+package cn.iocoder.springboot.lab21.springmvc.controller;
+
+import cn.iocoder.springboot.lab21.springmvc.constants.ServiceExceptionEnum;
+import cn.iocoder.springboot.lab21.springmvc.core.exception.ServiceException;
+import cn.iocoder.springboot.lab21.springmvc.core.vo.CommonResult;
+import cn.iocoder.springboot.lab21.springmvc.vo.UserVO;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.UUID;
+
+/**
+ * 用户 Controller
+ */
+@RestController
+@RequestMapping("/users")
+public class UserController {
+
+ /**
+ * 获得指定用户编号的用户
+ *
+ * 提供不使用 CommonResult 包装
+ *
+ * @param id 用户编号
+ * @return 用户
+ */
+ @GetMapping("/get")
+ public UserVO get(@RequestParam("id") Integer id) {
+ // 查询并返回用户
+ return new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
+ }
+
+ /**
+ * 获得指定用户编号的用户
+ *
+ * 提供使用 CommonResult 包装
+ *
+ * @param id 用户编号
+ * @return 用户
+ */
+ @GetMapping("/get2")
+ public CommonResult get2(@RequestParam("id") Integer id) {
+ // 查询用户
+ UserVO user = new UserVO().setId(id).setUsername(UUID.randomUUID().toString());
+ // 返回结果
+ return CommonResult.success(user);
+ }
+
+ /**
+ * 测试抛出 NullPointerException 异常
+ */
+ @GetMapping("/exception-01")
+ public UserVO exception01() {
+ throw new NullPointerException("没有粗面鱼丸");
+ }
+
+ /**
+ * 测试抛出 ServiceException 异常
+ */
+ @GetMapping("/exception-02")
+ public UserVO exception02() {
+ throw new ServiceException(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR);
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller2/TestController.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller2/TestController.java
new file mode 100644
index 000000000..766f0669c
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/controller2/TestController.java
@@ -0,0 +1,29 @@
+package cn.iocoder.springboot.lab21.springmvc.controller2;
+
+import cn.iocoder.springboot.lab21.springmvc.vo.UserVO;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.UUID;
+
+/**
+ * 测试 Controller
+ *
+ * 这个类的目的,主要是为了测试 {@link cn.iocoder.springboot.lab21.springmvc.core.web.GlobalResponseBodyHandler} ,不拦截处理这个包
+ */
+@RestController
+@RequestMapping("/test")
+public class TestController {
+
+ /**
+ * 获得指定用户编号的用户
+ *
+ * @return 用户
+ */
+ @GetMapping("/get")
+ public UserVO get() {
+ return new UserVO().setId(1).setUsername(UUID.randomUUID().toString());
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/exception/ServiceException.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/exception/ServiceException.java
new file mode 100644
index 000000000..226096b4c
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/exception/ServiceException.java
@@ -0,0 +1,50 @@
+package cn.iocoder.springboot.lab21.springmvc.core.exception;
+
+import cn.iocoder.springboot.lab21.springmvc.constants.ServiceExceptionEnum;
+
+/**
+ * 服务异常
+ *
+ * 参考 https://www.kancloud.cn/onebase/ob/484204 文章
+ *
+ * 一共 10 位,分成四段
+ *
+ * 第一段,1 位,类型
+ * 1 - 业务级别异常
+ * 2 - 系统级别异常
+ * 第二段,3 位,系统类型
+ * 001 - 用户系统
+ * 002 - 商品系统
+ * 003 - 订单系统
+ * 004 - 支付系统
+ * 005 - 优惠劵系统
+ * ... - ...
+ * 第三段,3 位,模块
+ * 不限制规则。
+ * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
+ * 001 - OAuth2 模块
+ * 002 - User 模块
+ * 003 - MobileCode 模块
+ * 第四段,3 位,错误码
+ * 不限制规则。
+ * 一般建议,每个模块自增。
+ */
+public final class ServiceException extends RuntimeException {
+
+ /**
+ * 错误码
+ */
+ private final Integer code;
+
+ public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
+ // 使用父类的 message 字段
+ super(serviceExceptionEnum.getMessage());
+ // 设置错误码
+ this.code = serviceExceptionEnum.getCode();
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/package-info.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/package-info.java
new file mode 100644
index 000000000..2df882fa5
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 提供核心封装
+ */
+package cn.iocoder.springboot.lab21.springmvc.core;
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/vo/CommonResult.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/vo/CommonResult.java
new file mode 100644
index 000000000..143a4b52b
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/vo/CommonResult.java
@@ -0,0 +1,102 @@
+package cn.iocoder.springboot.lab21.springmvc.core.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+
+/**
+ * 通用返回结果
+ *
+ * @param 结果泛型
+ */
+public class CommonResult implements Serializable {
+
+ public static Integer CODE_SUCCESS = 0;
+
+ /**
+ * 错误码
+ */
+ private Integer code;
+ /**
+ * 错误提示
+ */
+ private String message;
+ /**
+ * 返回数据
+ */
+ private T data;
+
+ /**
+ * 将传入的 result 对象,转换成另外一个泛型结果的对象
+ *
+ * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
+ *
+ * @param result 传入的 result 对象
+ * @param 返回的泛型
+ * @return 新的 CommonResult 对象
+ */
+ public static CommonResult error(CommonResult> result) {
+ return error(result.getCode(), result.getMessage());
+ }
+
+ public static CommonResult error(Integer code, String message) {
+ Assert.isTrue(!CODE_SUCCESS.equals(code), "code 必须是错误的!");
+ CommonResult result = new CommonResult<>();
+ result.code = code;
+ result.message = message;
+ return result;
+ }
+
+ public static CommonResult success(T data) {
+ CommonResult result = new CommonResult<>();
+ result.code = CODE_SUCCESS;
+ result.data = data;
+ result.message = "";
+ return result;
+ }
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public void setCode(Integer code) {
+ this.code = code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public T getData() {
+ return data;
+ }
+
+ public void setData(T data) {
+ this.data = data;
+ }
+
+ @JsonIgnore
+ public boolean isSuccess() {
+ return CODE_SUCCESS.equals(code);
+ }
+
+ @JsonIgnore
+ public boolean isError() {
+ return !isSuccess();
+ }
+
+ @Override
+ public String toString() {
+ return "CommonResult{" +
+ "code=" + code +
+ ", message='" + message + '\'' +
+ ", data=" + data +
+ '}';
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/web/GlobalExceptionHandler.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/web/GlobalExceptionHandler.java
new file mode 100644
index 000000000..04a6644f8
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/web/GlobalExceptionHandler.java
@@ -0,0 +1,58 @@
+package cn.iocoder.springboot.lab21.springmvc.core.web;
+
+import cn.iocoder.springboot.lab21.springmvc.constants.ServiceExceptionEnum;
+import cn.iocoder.springboot.lab21.springmvc.core.exception.ServiceException;
+import cn.iocoder.springboot.lab21.springmvc.core.vo.CommonResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+ private Logger logger = LoggerFactory.getLogger(getClass());
+
+ /**
+ * 处理 ServiceException 异常
+ */
+ @ResponseBody
+ @ExceptionHandler(value = ServiceException.class)
+ public CommonResult serviceExceptionHandler(HttpServletRequest req, ServiceException ex) {
+ logger.debug("[serviceExceptionHandler]", ex);
+ // 包装 CommonResult 结果
+ return CommonResult.error(ex.getCode(), ex.getMessage());
+ }
+
+ /**
+ * 处理 MissingServletRequestParameterException 异常
+ *
+ * SpringMVC 参数不正确
+ */
+ @ResponseBody
+ @ExceptionHandler(value = MissingServletRequestParameterException.class)
+ public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {
+ logger.debug("[missingServletRequestParameterExceptionHandler]", ex);
+ // 包装 CommonResult 结果
+ return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
+ ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
+ }
+
+ /**
+ * 处理其它 Exception 异常
+ */
+ @ResponseBody
+ @ExceptionHandler(value = Exception.class)
+ public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
+ // 记录异常日志
+ logger.error("[exceptionHandler]", e);
+ // 返回 ERROR CommonResult
+ return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
+ ServiceExceptionEnum.SYS_ERROR.getMessage());
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/web/GlobalResponseBodyHandler.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/web/GlobalResponseBodyHandler.java
new file mode 100644
index 000000000..c4ab3a538
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/core/web/GlobalResponseBodyHandler.java
@@ -0,0 +1,31 @@
+package cn.iocoder.springboot.lab21.springmvc.core.web;
+
+import cn.iocoder.springboot.lab21.springmvc.core.vo.CommonResult;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+// 只拦截我们的 Controller 所在包,避免其它类似 swagger 提供的 API 被切面拦截
+@ControllerAdvice(basePackages = "cn.iocoder.springboot.lab21.springmvc.controller")
+public class GlobalResponseBodyHandler implements ResponseBodyAdvice {
+
+ @Override
+ public boolean supports(MethodParameter returnType, Class converterType) {
+ return true;
+ }
+
+ @Override
+ public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
+ ServerHttpRequest request, ServerHttpResponse response) {
+ // 如果已经是 CommonResult 类型,则直接返回
+ if (body instanceof CommonResult) {
+ return body;
+ }
+ // 如果不是,则包装成 CommonResult 类型
+ return CommonResult.success(body);
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/vo/UserVO.java b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/vo/UserVO.java
new file mode 100644
index 000000000..c5033fcb5
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/main/java/cn/iocoder/springboot/lab21/springmvc/vo/UserVO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.springboot.lab21.springmvc.vo;
+
+/**
+ * 用户 VO
+ */
+public class UserVO {
+
+ /**
+ * 编号
+ */
+ private Integer id;
+ /**
+ * 账号
+ */
+ private String username;
+
+ public Integer getId() {
+ return id;
+ }
+
+ public UserVO setId(Integer id) {
+ this.id = id;
+ return this;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public UserVO setUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+}
diff --git a/lab-23/lab-springmvc-23-02/src/test/java/cn/iocoder/springboot/lab21/springmvc/package-info.java b/lab-23/lab-springmvc-23-02/src/test/java/cn/iocoder/springboot/lab21/springmvc/package-info.java
new file mode 100644
index 000000000..4a93fffd9
--- /dev/null
+++ b/lab-23/lab-springmvc-23-02/src/test/java/cn/iocoder/springboot/lab21/springmvc/package-info.java
@@ -0,0 +1 @@
+package cn.iocoder.springboot.lab21.springmvc;
diff --git a/lab-23/pom.xml b/lab-23/pom.xml
new file mode 100644
index 000000000..04dbe5aa1
--- /dev/null
+++ b/lab-23/pom.xml
@@ -0,0 +1,22 @@
+
+
+
+ labs-parent
+ cn.iocoder.springboot.labs
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ lab-23
+ pom
+
+ lab-23-01
+ lab-springmvc-23
+ lab-springmvc-23-01
+ lab-springmvc-23-02
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 3e948a30d..ac6a6f057 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,8 @@
lab-19
lab-20
lab-21
+ lab-22
+ lab-23