Java 企业级应用开发 part 7 - J2EE part 7

Spring MVC 高级功能

  • 异常处理
  • 拦截器
  • 文件上传/下载

异常处理

简单异常处理器

创建 ExceptionController

ExceptionController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.controller;

import com.itheima.exception.MyException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;

@Controller
public class ExceptionController {
//抛出空指针异常
@RequestMapping("showNullPointer")
public void showNullPointer() {
ArrayList<Object> list = new ArrayList<>();
System.out.println(list.get(2));
}
}

配置 SimpleMappingExceptionResolver

spring-mvc.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置创建 spring 容器要扫描的包 -->
<context:component-scan base-package="com.itheima.controller"/>
<!-- 配置注解驱动 -->
<mvc:annotation-driven/>
<!--配置静态资源的访问映射,此配置中的文件,将不被前端控制器拦截 -->
<mvc:resources mapping="/js/**" location="/js/"/>
<!-- 注入 SimpleMappingExceptionResolver-->
<bean class=
"org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!--
 定义需要特殊处理的异常,用类名或完全路径名作为key,对应的异常页面名作为值,
 将不同的异常映射到不同的页面上。
-->
<property name="exceptionMappings">
<props>
<prop key="java.lang.NullPointerException">
nullPointerExp.jsp
</prop>
<prop key="IOException">IOExp.jsp</prop>
</props>
</property>
</bean>
<!-- 配置视图解析器 -->
<bean class=
"org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>

创建异常处理页面

nullPointerExp.jsp
1
2
3
4
5
6
7
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>空指针异常处理页面</title></head>
<body>
空指针异常处理页面-----${exp}
</body>
</html>

自定义异常处理器

创建自定义异常类

MyException.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itheima.exception;

public class MyException extends Exception {
//异常信息
private String message;

public MyException(String message) {
super(message);
this.message = message;
}

@Override
public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

修改 ExceptionController

ExceptionController.java
1
2
3
4
@RequestMapping("addData")
public void addData() throws MyException {
throw new MyException("新增数据异常!");
}

创建自定义异常处理器

MyExceptionResolver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.itheima.controller;

import com.itheima.exception.MyException;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;

@Component
public class MyExceptionHandler implements HandlerExceptionResolver {
/**
* @param request 当前的 HTTP request
* @param response 当前的 HTTP response
* @param handler 正在执行的Handler
* @param ex handler执行时抛出的exception
* @return 返回一个ModelAndView
*/
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 定义异常信息
String msg;
//如果是自定义异常,将异常信息直接返回
if (ex instanceof MyException) {
msg = ex.getMessage();
} else {
// 如果是系统的异常,从堆栈中获取异常信息
Writer out = new StringWriter();
PrintWriter s = new PrintWriter(out);
ex.printStackTrace(s);
//系统真实异常信息,可以以邮件,短信等方式发给相关开发人员
String sysMsg = out.toString();
//向客户隐藏真实的异常信息,仅发送友好提示信息
msg = "网络异常!";
}
// 返回错误页面,给用户友好页面显示错误信息
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", msg);
modelAndView.setViewName("error.jsp");
return modelAndView;
}
}

创建异常处理页面

error.jsp
1
2
3
4
5
6
7
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>异常处理页面</title></head>
<body>
${msg}
</body>
</html>

异常处理注解

创建 ExceptionAdvice

ExceptionAdvice.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.itheima.controller;

import com.itheima.exception.MyException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;

@ControllerAdvice
public class ExceptionAdvice {
//处理MyException类型的异常
@ExceptionHandler(MyException.class)
public ModelAndView doMyException(MyException ex) throws IOException {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", ex.getMessage());
modelAndView.setViewName("error.jsp");
return modelAndView;
}

//处理Exception类型的异常
@ExceptionHandler(Exception.class)
public ModelAndView doOtherException(Exception ex) throws IOException {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", "网络异常!");
modelAndView.setViewName("error.jsp");
return modelAndView;
}
}

值得注意的是,@ControllerAdvice 注解的类中定义的 @ExceptionHandler 方法会优先于 HandlerExceptionResolver 处理异常。

拦截器

自定义拦截器

创建 MyInterceptor

MyInterceptor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.itheima.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 实现了HandlerInterceptor接口的自定义拦截器
*/
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
System.out.println("MyInterceptor...preHandle");
//对拦截的请求进行放行处理
return true;
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
System.out.println("MyInterceptor...postHandle");
}

@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception ex) {
System.out.println("MyInterceptor...afterCompletion");
}
}

配置 MyInterceptor 拦截器

spring-mvc.xml
1
2
3
4
5
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 拦截器1 -->
<bean class="com.itheima.interceptor.MyInterceptor"/>
</mvc:interceptors>

其中,preHandle 方法在控制器方法执行前被调用,postHandle 方法在控制器方法正常执行后被调用,afterCompletion 方法在整个请求完成后被调用。

对于大型项目,可能需要多个拦截器来处理不同的请求,这时可以在 <mvc:interceptors> 标签中配置多个 <bean> 来定义多个拦截器。

我们新增一个拦截器

MyInterceptor2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.itheima.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 实现了HandlerInterceptor接口的自定义拦截器
*/
public class MyInterceptor2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
System.out.println("MyInterceptor2...preHandle");
//对拦截的请求进行放行处理
return true;
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
System.out.println("MyInterceptor2...postHandle");
}

@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception ex) {
System.out.println("MyInterceptor2...afterCompletion");
}
}

配置 MyInterceptor2 拦截器

spring-mvc.xml
1
2
3
4
5
6
7
<!-- 配置拦截器 -->
<mvc:interceptors>
<!-- 拦截器1 -->
<bean class="com.itheima.interceptor.MyInterceptor"/>
<!-- 拦截器2 -->
<bean class="com.itheima.interceptor.MyInterceptor2"/>
</mvc:interceptors>

当有多个拦截器时,preHandle 方法会按照配置的顺序依次执行,而 postHandleafterCompletion 方法则会按照相反的顺序执行。

文件上传/下载

fileload.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>文件上传和下载</title>
<script src="${ pageContext.request.contextPath }/js/jquery-3.6.0.js" type="text/javascript"></script>
</head>
<body>
<table border="1">
<tr>
<td width="200" align="center">文件上传${msg}</td>
<td width="300" align="center">下载列表</td>
</tr>
<tr>
<td height="100">
<form action="${pageContext.request.contextPath}/fileload"
method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple="multiple"><br/>
<input type="reset" value="清空"/>
<input type="submit" value="提交"/>
</form>
</td>
<td id="files"></td>
</tr>
</table>
</body>
<script>
$(document).ready(function () {
var url = "${pageContext.request.contextPath }/getFilesName";
$.get(url, function (files) {
var files = eval('(' + files + ')');
for (var i = 0; i < files.length; i++) {
$("#files").append("<li>" +
"<a href=${pageContext.request.contextPath }" + "" +
"\\" + "download?filename=" + files[i].name + ">" +
files[i].name + "</a></li>");
}
})
})
</script>
</html>

通过表单上传文件,Spring MVC 提供了 MultipartFile 接口来处理文件上传。我们可以在控制器方法中使用 MultipartFile 参数来接收上传的文件。

需要配置 MultipartResolver 以启用文件上传功能:

spring-mvc.xml
1
2
3
4
5
6
7
<bean id="multipartResolver" class=
"org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置请求编码格式,必须与JSP中的pageEncoding属性一致,默认为ISO-8859-1 -->
<property name="defaultEncoding" value="UTF-8"/>
<!-- 设置允许上传文件的最大值为2M,单位为字节 -->
<property name="maxUploadSize" value="2097152"/>
</bean>

由于依赖 Apache Commons FileUpload 组件来处理文件上传,因此需要在项目中添加相关依赖:

pom.xml
1
2
3
4
5
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>

创建资源类

Resource.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itheima.pojo;

public class Resource {
private String name; //name属性表示文件名称

public Resource() {
}

public Resource(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

创建 JSON 文件工具类

JSONFileUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.utils;

import org.apache.commons.io.IOUtils;

import java.io.FileInputStream;
import java.io.FileOutputStream;

public class JSONFileUtils {
public static String readFile(String filepath) throws Exception {
FileInputStream fis = new FileInputStream(filepath);
return IOUtils.toString(fis);
}

public static void writeFile(String data, String filepath)
throws Exception {
FileOutputStream fos = new FileOutputStream(filepath);
IOUtils.write(data, fos);
}
}

创建文件管理控制器

FileController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.itheima.controller;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itheima.pojo.Resource;
import com.itheima.utils.JSONFileUtils;
import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

@Controller
public class FileController {
/**
* 文件上传
*/
@RequestMapping("fileload")
public String fileLoad(MultipartFile[] files,
HttpServletRequest request) throws Exception {
//设置上传的文件所存放的路径
String path = request.getServletContext().getRealPath("/") + "files/";
ObjectMapper mapper = new ObjectMapper();
if (files != null && files.length > 0) {
//循环获取上传的文件
for (MultipartFile file : files) {
//获取上传文件的名称
String filename = file.getOriginalFilename();
ArrayList<Resource> list = new ArrayList<>();
//读取files.json文件中的文件名称
String json = JSONFileUtils.readFile(path + "/files.json");
if (json.length() != 0) {
//将files.json的内容转为集合
list = mapper.readValue(json,
new TypeReference<List<Resource>>() {
});
for (Resource resource : list) {
//如果上传的文件在files.json文件中有同名文件,将当前上传的文件重命名,以避免重名
if (filename.equals(resource.getName())) {
String[] split = filename.split("\\.");
filename = split[0] + "(1)." + split[1];
}
}
}
// 文件保存的全路径
String filePath = path + filename;
// 保存上传的文件
file.transferTo(new File(filePath));
list.add(new Resource(filename));
json = mapper.writeValueAsString(list); //将集合中转换成json
//将上传文件的名称保存在files.json文件中
JSONFileUtils.writeFile(json, path + "/files.json");
}
request.setAttribute("msg", "(上传成功)");
return "forward:fileload.jsp";
}
request.setAttribute("msg", "(上传失败)");
return "forward:fileload.jsp";
}

@ResponseBody
@RequestMapping(value = "/getFilesName",
produces = "text/html;charset=utf-8")
public String getFilesName(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String path = request.getServletContext().
getRealPath("/") + "files/files.json";
String json = JSONFileUtils.readFile(path);
return json;
}

/**
* 根据浏览器的不同进行编码设置,返回编码后的文件名
*/
public String getFileName(HttpServletRequest request,
String filename) throws Exception {
// 替换废弃的sun.misc.BASE64Encoder为JDK标准的java.util.Base64
Base64.Encoder base64Encoder = Base64.getEncoder();
String agent = request.getHeader("User-Agent");
if (agent.contains("Firefox")) {
// 火狐浏览器
filename = "=?UTF-8?B?" +
new String(base64Encoder.encode(filename.getBytes(StandardCharsets.UTF_8))) +
"?=";
} else {
// IE及其他浏览器
filename = URLEncoder.encode(filename, "UTF-8");
}
return filename;
}

/**
* 文件下载
*/
@RequestMapping("/download")
public ResponseEntity<byte[]> fileDownload(HttpServletRequest request,
String filename) throws Exception {
// 指定要下载的文件所在路径
String path = request.getServletContext().getRealPath("/files/");
filename = new String(filename.getBytes("ISO-8859-1"), "UTF-8");
// 创建该文件对象
File file = new File(path + File.separator + filename);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
filename = this.getFileName(request, filename);
// 通知浏览器以下载的方式打开文件
headers.setContentDispositionFormData("attachment", filename);
// 定义以流的形式下载返回文件数据
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 使用Sring MVC框架的ResponseEntity对象封装返回下载数据
return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),
headers, HttpStatus.OK);
}
}