MVC
-
Spring MVC用于实现Servlet标准的Web接口
- MVC通过封装原生Servlet对HTTP请求的API,极大地简化了控制器层的开发
- 每一个HTTP请求由Servlet 容器的线程池为其分配一个线程,请求结束后线程自动回收
-
SpringBoot引入web启动器即可
xml
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
HTTP协议传输
-
短连接与长连接
- 短连接之间互相独立,HTTP请求是典型的长连接协议
- 长连接之间可以互相感知,WebSocket请求是典型的双向实时通信的长连接请求
- 短连接请求适用于【请求-响应】交互场景,长连接请求适用于【维持通信】的场景
-
HTTP请求和HTTPS请求
-
HTTP 是超文本明文传输,HTTPS在【TPC/IP传输层】 和 【HTTP 应用层】之间加入了 SSL/TLS 安全协议,实现加密传输
-
HTTP 的端口号是 80,HTTPS 的端口号是 443
-
HTTPS 协议需要向 CA 申请数字证书,来保证服务器的身份是可信的
-
请求结构
-
请求结构:标准的HTTP请求包含了三部分,请求行、请求头、请求体
-
请求行结构:
<Method(请求方法)> <Request-URI(请求资源)> <HTTP-Version(请求协议)>Method:设置请求方法,常见有GET、PUT、DELETE、POSTRequest-URI:请求资源路径,查询参数用?或/连接HTTP-Version:协议版本
-
请求头结构:以键值对形式存储数据,用于传递与请求或客户端相关的额外信息
请求头key 说明 举例 User-Agent标识客户端类型 Mozilla/5.0 (Windows NT 10.0; Win64; x64)Authorization传递认证凭证 Bearer <token>、Basic <base64(username:password)>Cookie用户相关参数 {sessionid=abc123, user_token=xyz456}Content-Type请求体的数据类型 application/json、multipart/form-dataContent-Length请求体长度 1024Accept接收的响应类型 text/html、application/json、image/pngX-自定义请求头约定 X-开头X-token=122345665 -
请求体:由请求头的属性
Content-Type指定,一般是json,其他常见的有文件类型等
apl
POST adduser/1 HTTP/1.1 # 请求行
Content-Type: application/json # 请求头
{
"name": "Alice",
"age": 25, # 请求体
"email": "alice@example.com"
}
响应结构
-
一个标准的响应包括三部分:响应状态码、响应头、响应体
-
响应行:协议版本 + 响应状态码
-
响应头:以键值对形式存储数据
响应头key 说明 举例 Content-Type响应体的数据类型 application/json、application/pdf、image/pngContent-Length响应体的数据长度 1024Content-Disposition浏览器处理响应体方式 attachment; filename="example.pdf"(不直接显式下载的文件)Location重定向目标 URL Location: https://example.com/new-pathHeader自定义响应头 X-Version:1.0 -
响应体:数据类型和字符集由响应头
Content-Type指定,一般响应体中的数据类型是json,文件类型见下方文件传输
apl
HTTP/1.1 200 OK # 状态码
Content-Type: application/json # 响应头
{
"name": "Alice",
"age": 25, # 响应体
"email": "alice@example.com"
}
MVC映射
-
注解匹配请求
- 请求方法:
@getMapping、@postMapping、@putMapping、@deleteMapping - 请求参数:
@RequestParam(入参可为空),@PathVariable(入参不为空)、@DateTimeFormat(pattern = "yyyy-MM-dd") - 请求头:
@RequestHeader、@CookieValue - 请求体:
@requestBody会将请求体反Json序列化为指定实例
- 请求方法:
-
HttpServletRequest API
- 请求头:
getHeader、getCookies - 请求体:
getReader()
- 请求头:
-
注解匹配响应
- 响应码:
@ResponseStatus - 响应体:
@ResponseBody会将方法返回值自动序列化为Json字符串写入响应体
- 响应码:
-
HttpServletRequest API
- 响应码:
setStatus() - 响应头:
setHeader()、setContentType()、setCharacterEncoding() - 响应体:
getWriter().write()、ImageIO.write()
- 响应码:
-
ResponseEntity
- 响应码:
.status() - 响应头:
.header()、.contentType() - 响应体:
.body()
- 响应码:
-
最佳实践
- 请求方法:注解映射
- 请求路径参数:注解映射
- 请求体:注解映射
- 请求头:【HttpServletRequest API】映射
- 响应头:【HttpServletRequest API】形式映射
- 响应体:注解形式映射
- 简单场景推荐使用
ResponseEntity
java
@RestController // @ResponseBody响应体自动转化为JSON字符串
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/*
GET /user/getInfo?id=1 HTTP/1.1
User-Agent:Mozilla/5.0
Cookies: [Uid=12d32,Uname=wyh]
{
"name": "Alice",
"age": 25,
"email": "alice@example.com"
}
*/
@GetMapping("/getInfo")
public ResultJson<User> getInfo( // 使用注解形式
@RequestParam(value = "id", required = false) Long userId, // @RequestParam匹配请求?连接参数,id可以不传参
@CookieValue("Uname") String name, // @CookieValue 注解匹配cookie字段
@RequestBody User user) // @RequestBody 注解匹配请求体
{
System.out.println(name); //wyh
System.out.println(user.getName()); //Alice
return new ResultJson<User>(200,new User());
}
/*
GET /user/getUser/{id} HTTP/1.1
User-Agent:Mozilla/5.0
Cookies: [Uid=12d32,Uname=wyh]
*/
@GetMapping("/getUser")
public void getUser(HttpServletRequest request, HttpServletResponse response) { // 使用原生servlet API,返回void
Cookie[] cookies = request.getCookies(); // 获取cookies
Integer id;
for (Cookie cookie : cookies) {
if ("id".equals(cookie.getName())) { // 从cookies中获取id
id = Long.parseLong(cookie.getValue());
break;
}
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE); // MediaType新版不再包含字符集声明
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 单独设置编码
if(id == null){
response.getWriter().write(JSON.toJSONString(Result.fail("无效的 id 格式"))); //写入响应体
}
User user = userService.getUserById(id);
response.setHeader("X-Version", "1.0"); //写入自定义响应头
response.setStatus(HttpServletResponse.SC_OK); //写入响应码
response.getWriter().write(JSON.toJSONString(Result.ok(user))); //写入响应体
// 不需要手动关闭IO流,因为 Servlet 容器会在响应结束后自动关闭底层流
}
/*
GET /user/getUser2/{id} HTTP/1.1
User-Agent:Mozilla/5.0
*/
@GetMapping("/getUser2/{id}")
public ResponseEntity<User> getUser2(@PathVariable("id") Long id) { // ResponseEntity泛型控制响应体的类型
ResponseEntity<User> body = ResponseEntity.status(200)
.header("X-Request-ID", UUID.randomUUID().toString())
.header("X-Version", "1.0")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new ResultJson(200,new User())); // 响应体会自动序列化为JSON字符串
return body;
}
/*
PUT /user/update/666 HTTP/1.1
Content-Type: application/json
{
"name": "Alice",
"age": 25,
"email": "alice@example.com"
}
*/
@PutMapping("/updateuser/{id}") //请求体JSON数据,也可以映射为Map
public void updateUser(@PathVariable("id") Integer userId, @RequestBody Map<String, Object> map){
System.out.println(map.get("name")); //Alice,Json映射为Map
}
}
统一返回体
-
描述:无论是成功还是失败,后端给前端返回的数据结构是相同的
- 强制所有接口返回统一格式,避免因开发者习惯不同导致响应结构混乱,前端也便于统一处理响应
- 业务逻辑上的异常返回200,服务器异常返回5xx/4xx
java
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Result<T> {
private Boolean status; // 状态标识,快速判断是否正常响应,如果异常再查自定义响应码和描述信息
private String code; // 自定义内部状态码
private String msg; // 描述信息
private T data; // 响应体数据
public static <T> Result<T> ok(){
return new Result<>(true, "200", null, null);
}
public static <T> Result<T> ok(T data){
return new Result<>(true, "200", null, data);
}
public static <T> Result<T> fail(String msg, String code){
return new Result<>(false, code, msg, null);
}
}
- 请求体Json映射VO使用Jacskon反序列化,默认相同名称映射,也可以自定义映射规则
java
@Data
@ToString
@AllArgsConstructor
public class User{
@JsonProperty("id") // 映射Json数据的"id"键
private Integer userId;
@JsonProperty("name") // 映射Json数据的"name"键
private String userName;
@JsonProperty("email") // 映射Json数据的"email"键
private String userEmail;
@JsonProperty("time") // 映射Json数据的"time"键
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh") // 指定映射规则
private Date time; // 时间类型还是建议以Long类型传递
}
封装HTTP上下文
- 将业务涉及HTTP数据封装为【Http上下文】,常见于微服务架构,用于对所有子微服务统一管理
java
/**
* 精简版HttpContext - 核心功能封装
*/
public interface HttpContext {
// ========== 请求相关 ==========
/** 获取原始请求 */
HttpServletRequest getRequest();
/** 获取原始响应 */
HttpServletResponse getResponse();
/** 获取请求方法 */
String getMethod();
/** 获取请求路径 */
String getPath();
/** 获取客户端IP */
String getClientIp();
/** 获取请求头 */
String getHeader(String name);
/** 获取所有请求头 */
Map<String, String> getHeaders();
/** 获取Query参数 */
String getQueryParam(String name);
/** 获取所有Query参数 */
Map<String, String> getQueryParams();
/** 获取请求体(字符串) */
String getRequestBody();
/** 获取请求体并解析为对象 */
<T> T getRequestBody(Class<T> clazz);
/** 获取上传文件 */
<T> T getFile(String name, Class<T> fileType);
// ========== 响应相关 ==========
/** 设置状态码 */
void setStatus(int statusCode);
/** 设置响应头 */
void setHeader(String name, String value);
/** 设置Content-Type */
void setContentType(String contentType);
/** 写入响应体 */
void write(String content);
/** 写入JSON响应 */
void writeJson(Object data);
/** 写入成功响应 */
void writeSuccess(Object data);
/** 写入错误响应 */
void writeError(int code, String message);
/** 重定向 */
void redirect(String url);
/** 是否已提交响应 */
boolean isCommitted();
// ========== 上下文属性 ==========
/** 设置属性 */
void setAttribute(String key, Object value);
/** 获取属性 */
<T> T getAttribute(String key);
/** 移除属性 */
void removeAttribute(String key);
}
java
/**
* 标准HttpContext实现
*/
public class HttpContext implements HttpContext {
private final HttpServletRequest request;
private final HttpServletResponse response;
private final Map<String, Object> attributes = new ConcurrentHashMap<>();
// 缓存数据
private String requestBody;
private Map<String, String> queryParams;
private Map<String, String> headers;
// 响应状态
private boolean committed = false;
private String responseBody;
private int status = 200;
// 元数据
private final String requestId;
private final long startTime;
public HttpContext(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
}
// ========== 请求相关 ==========
@Override
public HttpServletRequest getRequest() {
return request;
}
@Override
public HttpServletResponse getResponse() {
return response;
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public String getPath() {
return request.getRequestURI();
}
@Override
public String getClientIp() {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
@Override
public String getHeader(String name) {
return request.getHeader(name);
}
@Override
public Map<String, String> getHeaders() {
if (headers == null) {
headers = new LinkedHashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, request.getHeader(name));
}
}
return headers;
}
@Override
public String getQueryParam(String name) {
return request.getParameter(name);
}
@Override
public Map<String, String> getQueryParams() {
if (queryParams == null) {
queryParams = new LinkedHashMap<>();
Map<String, String[]> params = request.getParameterMap();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
String[] values = entry.getValue();
if (values != null && values.length > 0) {
queryParams.put(entry.getKey(), values[0]);
}
}
}
return queryParams;
}
@Override
public String getRequestBody() {
if (requestBody == null) {
StringBuilder body = new StringBuilder();
try (BufferedReader reader = request.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
requestBody = body.toString();
} catch (IOException e) {
throw new RuntimeException("Failed to read request body", e);
}
}
return requestBody;
}
@Override
public <T> T getRequestBody(Class<T> clazz) {
String body = getRequestBody();
if (body == null || body.isEmpty()) {
return null;
}
try {
// 伪代码,实际应使用JSON库
return parseJson(body, clazz);
} catch (Exception e) {
throw new RuntimeException("Failed to parse request body", e);
}
}
@Override
@SuppressWarnings("unchecked")
public <T> T getFile(String name, Class<T> fileType) {
// 简化处理,实际应根据fileType返回不同类型的文件对象
if (request instanceof org.springframework.web.multipart.MultipartHttpServletRequest) {
org.springframework.web.multipart.MultipartHttpServletRequest multipartRequest =
(org.springframework.web.multipart.MultipartHttpServletRequest) request;
return (T) multipartRequest.getFile(name);
}
return null;
}
// ========== 响应相关 ==========
@Override
public void setStatus(int statusCode) {
this.status = statusCode;
response.setStatus(statusCode);
}
@Override
public void setHeader(String name, String value) {
response.setHeader(name, value);
}
@Override
public void setContentType(String contentType) {
response.setContentType(contentType);
}
@Override
public void write(String content) {
if (committed) {
throw new IllegalStateException("Response already committed");
}
try {
response.setStatus(status);
PrintWriter writer = response.getWriter();
writer.write(content);
writer.flush();
this.responseBody = content;
this.committed = true;
} catch (IOException e) {
throw new RuntimeException("Failed to write response", e);
}
}
@Override
public void writeJson(Object data) {
setContentType("application/json;charset=UTF-8");
String json = toJson(data);
write(json);
}
@Override
public void writeSuccess(Object data) {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("code", 200);
result.put("data", data);
result.put("timestamp", System.currentTimeMillis());
writeJson(result);
}
@Override
public void writeError(int code, String message) {
setStatus(code);
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("code", code);
result.put("message", message);
result.put("timestamp", System.currentTimeMillis());
writeJson(result);
}
@Override
public void redirect(String url) {
if (committed) {
throw new IllegalStateException("Response already committed");
}
try {
response.sendRedirect(url);
this.committed = true;
} catch (IOException e) {
throw new RuntimeException("Failed to redirect", e);
}
}
@Override
public boolean isCommitted() {
return committed;
}
// ========== 上下文属性 ==========
@Override
public void setAttribute(String key, Object value) {
attributes.put(key, value);
}
@Override
@SuppressWarnings("unchecked")
public <T> T getAttribute(String key) {
return (T) attributes.get(key);
}
@Override
public void removeAttribute(String key) {
attributes.remove(key);
}
}
文件传输
- 描述:请求体是文件数据【文件上传】,响应体是文件数据【文件下载】
yaml
# SpringBoot默认已配置MultipartResolver的Bean,可以使用统一配置文件或者配置类修改属性
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 1MB
enabled: true
location: /tmp/uploads
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(10485760); // 10MB
resolver.setDefaultEncoding("UTF-8");
return resolver;
}
}
文件上传
- 前端一般通过表单传输文件,一个键代表一种类型的文件,可以是一个,也可以是多个文件
- 后端mvc接收:使用
MultipartFile类接收,使用@RequestParam指定文件(组)
java
@RestController
@RequestMapping("user")
public class UserController {
@PostMapping("upload-file")
public void insertFiles(
@RequestParam("files") MultipartFile[] files,
@RequestParam("file") MultipartFile file
) {
// 1. 检查并创建上传目录
if (!new File("upload").mkdir()) {
throw new RuntimeException("mkdir failed");
}
// 2. 处理单个文件(files-2)
if (!file.isEmpty()) {
saveFile(file, "upload");
}
// 3. 处理多个文件(files)
for (MultipartFile file : files) {
if (!file.isEmpty()) {
saveFile(file, "upload");
}
}
}
/**
* 保存文件到指定目录(避免代码重复)
* @param file MultipartFile 对象
* @param uploadDir 目标目录
*/
private void saveFile(MultipartFile file, String uploadDir) {
try {
// 1. 获取文件名
String originalFilename = file.getOriginalFilename(); // 可能返回 "C:\Users\test.jpg"
// 2. 防止路径遍历攻击
String safeFilename = Paths.get(originalFilename).getFileName().toString(); //去除路径,即"test.jpg"
Path targetPath = Paths.get(uploadDir, safeFilename); //组成文件路径
// 3. 使用 NIO 方式保存文件(更高效)
Files.copy(
file.getInputStream(),
targetPath,
StandardCopyOption.REPLACE_EXISTING // 如果文件已存在则覆盖
);
} catch (IOException e) {
throw new RuntimeException("文件保存失败: " + file.getOriginalFilename(), e);
}
}
}
文件下载
java
@RestController
@RequestMapping("user")
public class UserController {
@GetMapping("{id}/show-file") //原生HttpServletResponse构造响应
public void showFile(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
// 返回的文件
File file = new File("upload/JavaSE.pdf");
// 设置响应头
response.setContentType("application/pdf");
// 客户端默认直接显示文件需要手动下载,可以设置为直接下载文件
response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
response.setContentLength((int) file.length());
// 读取文件并写入响应输出流
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[4096];
while (true) {
int length = inputStream.read(buffer);
if (length == -1) {
break;
}
outputStream.write(buffer, 0, length);
outputStream.flush();
}
}
}
@GetMapping("{id}/download-file") //ResponseEntity构造响应,普通文件建议使用Resource泛型
public ResponseEntity<Resource> downLoadFile(@PathVariable("id") Long id) throws IOException {
Resource resource = new UrlResource("upload/JavaSE.pdf");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM) // 通用二进制流类型
//设置为直接下载文件
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
@GetMapping("{id}/download-bigfile") //ResponseEntity构造响应,大文件使用fileInputStream泛型,防止内存溢出
public ResponseEntity<InputStreamResource> downLoadBigFile(@PathVariable("id") Long id) throws IOException {
// 返回的文件
File file = new File("upload/JavaSE.pdf");
FileInputStream fileInputStream = new FileInputStream(file);
// 设置内容类型和响应头
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
//设置为直接下载文件
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
.body(new InputStreamResource(fileInputStream));
}
}
Web配置类
-
目的:如果要进一步设置Spring MVC高级功能,可以通过实现
WebMvcConfigurer接口来创建MVC配置类 -
使用方法
- 实现组件接口,编写逻辑内容
- 在
WebMvcConfigurer配置类中的重写方法中引用组件接口
-
常用组件方法
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { //静态资源处理器,直接返回本地资源
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/public/") // 本地资源
.setCachePeriod(3600); // 缓存时间(秒)
}
@Override
public void addViewControllers(ViewControllerRegistry registry) { //跳转指定页面
registry.addViewController("/home").setViewName("home"); // 访问/home跳转到home.html
registry.addRedirectViewController("/old", "/new"); // 重定向
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { //信息转换器
converters.add(new FastJsonHttpMessageConverter()); // 使用FastJson转换
}
}
拦截器
-
目的:在控制器接收请求前,拦截请求,先进行指定逻辑语句
-
替代方案
- 过滤器:Servlet容器规范组件,不依赖于Spring框架
- AOP:业务层级别的接口边界控制
java
//拦截器
public class Interceptor01 implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler //handler一般为HandlerMethod子类,用于获取请求的控制器方法
) throws Exception {
//TODO
return true; //返回true代表请求会继续向下执行,返回false会拦截请求直接返回响应
}
@Override
public void postHandle( // 控制器方法执行后(响应还未返回客户端)
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView
) throws Exception {
//TODO
}
@Override
public void afterCompletion( // 请求完成时(响应已返回客户端)
HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex
) throws Exception {
//TODO
}
}
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截器1
registry.addInterceptor(new Interceptor01())
.addPathPatterns("/**") //拦截哪些请求
.excludePathPatterns( //不拦截哪些请求
"/user/code",
"/voucher/**"
)
.order(0); //order设置执行顺序,值越小越先执行
//拦截器2
registry.addInterceptor(new Interceptor02()).order(1);
}
}
- 举例:动态参数的路径跳转
java
public class MoveInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
response.sendRedirect("/newAddress"); // 302 重定向,实现网址迁移
return false; // 终止后续处理
}
}
跨域处理
-
描述:前端访问后端时,因为同源规则导致后端禁止前端访问
-
原理
- 同源指的是:用户访问前端的请求 和 前端发送给后端的请求 之间的域名、端口、协议必须一样
- 跨域访问本质上不是错误,而是浏览器的限制行为(例如React访问Tomcat,但二者部署在不同的IP上)
-
解决方法
- 前端直接设位置服务器代理
- 前端由Nginx托管,一定是同源的
- 服务/网关设置跨域
全局统一配置(推荐)
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配指定访问路径,一般匹配所有请求
.allowedOrigins("*") // 浏览器允许所有的域访问,注意allowCredentials(true)时,allowedOrigins不能为 *
.allowCredentials(true) // 允许带cookie访问
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") // 允许跨域请求携带认证凭据 (如Cookie、Authorization、SSL 证书)
.maxAge(3600); // 预检请求(OPTIONS)的缓存时间(秒)
}
}
局部跨域设置
java
//作用于控制器方法
@CrossOrigin(origins = {"http://localhost:8080", "http://localhost:8081"})
@getMapping("test")
public String test() {
return "ok";
}
//作用于控制器类
@RestController
@RequestMapping("user")
@CrossOrigin(origins = {"*"}) //表示所有域都可以访问
public class TestController {
...
}
全局异常处理器
-
描述:对Controller层异常处理的兜底逻辑
- 业务层异常一般会一直传递到控制器层,因此可以认为是整个Web项目的异常兜底
- 异步任务等场景异常默认不会向上传递,全局异常处理器无法捕获,应内部处理或者
CompleteFuture链式处理 - servlet前置操作(如过滤器)用于请求未到达控制器层,其中抛出的异常也不会捕获,应内部处理
-
注解说明
@ControllerAdvice:指定异常扫描路径@ExceptionHandler:用于指定异常类型@RestControllerAdvice = @ControllerAdvice + @ResponseBody
java
@RestControllerAdvice(basePackages = {"com.wyh.controller"})
@Slf4j
public class ControllerExceptionHandler {
//处理权限异常,可以一次定义多个:@ExceptionHandler({xxxException.class, xxxException.class})
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ResultDto<Object>> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(403)
.contentType(MediaType.APPLICATION_JSON)
.body(new ResultDto<>("500", e.getCause().getClass().toString(), null));
}
// 处理请求体校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<JsonResult> handleValidException(MethodArgumentNotValidException ex) {
JsonResult fail = JsonResult.fail("method valid fail", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(fail);
}
// 处理参数校验异常
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleValidationException(ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String fieldName = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.put(fieldName, message);
});
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(Exception.class)
public void globalHandle(HttpServletResponse res, Exception e) {
res.setContentType("application/json; charset=utf-8");
ResultDto<Object> resultDto = new ResultDto<>("500", e.getCause().getClass().toString(), null);
String jsonResult = JSON.toJSONString(resultDto);
try {
res.getWriter().write(jsonResult);
} catch (IOException ex) { // Exception处理器是最后的兜底,不能再抛异常了
log.error(ex.getMessage(), ex);
}
}
}