}
modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
1.5.3、完成处理方法
拦截器最后执行的方法,无论原始方法是否执行
/**
-
afterCompletion方法会在最后执行
-
@param request 当前请求对象
-
@param response 响应对象
-
@param handler 相当于是真正能够处理请求的handler方法封装成的对象,对象中有这方法的相关信息
-
@param ex 异常对象
-
@throws Exception
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println(“afterCompletion”);
}
ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
1.6.1需求
我们的接口需要做用户登录状态的校验,如果用户没有登录则跳转到登录页面,登录的情况下则可以正常访问我们的接口。
1.6.2 分析
怎么判断是否登录?
登录时往session写入用户相关信息,然后在其他请求中从session中获取这些信息,如果获取不到说明不是登录状态。
很多接口都要去写判断的代码,难道在每个Handler中写判断逻辑?
用拦截器,在拦截器中进行登录状态的判断。
登录接口是否应该进行拦截?
不能拦截
静态资源是否要进行拦截?
不能拦截
1.6.3 步骤分析
①登录页面,请求发送给登录接口
②登录接口中,校验用户名密码是否正确(模拟校验即可,先不查询数据库)。
如果用户名密码正确,登录成功。把用户名写入session中。
③定义其他请求的Handler方法
④定义拦截器来进行登录状态判断
如果能从session中获取用户名则说明是登录的状态,则放行
如果获取不到,则说明未登录,要跳转到登录页面。
1.6.4 代码实现
1.6.4.1 登录功能代码实现
①编写登录页面
用户名:
密码:
②编写登录接口
接口中,校验用户名密码是否正确(模拟校验即可,先不查询数据库)。如果用户名密码正确,登录成功。把用户名写入session中。
@Controller
public class LoginController {
@PostMapping(“/login”)
public String longin(String username, String password, HttpSession session){
//往session域中写入用户名用来代表登录成功
session.setAttribute(“username”,username);
return “/WEB-INF/page/success.jsp”;
}
}
1.6.4.2 登录状态校验代码实现
①定义拦截器
public class LoginInterceptor implements HandlerInterceptor {
}
②重写方法,在preHandle方法中实现状态校验
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从session中获取用户名,判断是否存在
HttpSession session = request.getSession();
String username = (String) session.getAttribute(“username”);
if(StringUtils.isEmpty(username)){
//如果获取不到说明未登录 ,重定向跳转到登录页面
String contextPath = request.getServletContext().getContextPath();
response.sendRedirect(contextPath+“/static/login.html”);
}else{
//如果获取到了,说明之前登录过。放行。
return true;
}
return false;
}
}
③配置拦截器
-
登录相关接口不应该拦截
-
静态资源不拦截
mvc:interceptors
mvc:interceptor
<mvc:mapping path=“/**”/>
<mvc:exclude-mapping path=“/static/**”></mvc:exclude-mapping>
<mvc:exclude-mapping path=“/WEB-INF/page/**”></mvc:exclude-mapping>
<mvc:exclude-mapping path=“/login”></mvc:exclude-mapping>
</mvc:interceptor>
</mvc:interceptors>
如果我们配置了多个拦截器,拦截器的顺序是按照配置的先后顺序的。
这些拦截器中方法的执行顺序如图(preHandler都返回true的情况下):
执行顺序为:
preHandle1、preHandle2、preHandle3
testPath
postHandle3、postHandle2、postHandle1
afterCompletion3、afterCompletion2、afterCompletion1
如果拦截器3的preHandle方法返回值为false。执行顺序如图:
-
只有所有拦截器都放行了,postHandle方法才会被执行。
-
只有当前拦截器放行了,当前拦截器的afterCompletion方法才会执行。
执行顺序为:
preHandle1、preHandle2、preHandle3
afterCompletion2、afterCompletion1
==========================================================================
-
我们在实际项目中Dao层和Service层的异常都会被抛到Controller层。但是如果我们在Controller的方法中都加上异常的try…catch处理也会显的非常的繁琐。
-
所以SpringMVC为我们提供了统一异常处理方案。可以把Controller层的异常进行统一处理。这样既提高了代码的复用性也让异常处理代码和我们的业务代码解耦。
-
一种是实现
HandlerExceptionResolver
接口的方式,一种是使用@ControllerAdvice
注解的方式。
①实现接口
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
}
②重写方法
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
//如果handler中出现了异常,就会调用到该方法,我们可以在本方法中进行统一的异常处理。
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
//获取异常信息,把异常信息放入域对象中
String msg = ex.getMessage();
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject(“msg”,msg);
//跳转到error.jsp
modelAndView.setViewName(“/WEB-INF/page/error.jsp”);
return modelAndView;
}
}
③注入容器
可以使用注解注入也可以使用xml配置注入。这里使用注解注入的方式。在类上加 @Component 注解,注意要保证类能被组件扫描扫描到。
@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
//…省略无关代码
}
①创建类加上@ControllerAdvice注解进行标识
@ControllerAdvice
public class MyControllerAdvice {
}
②定义异常处理方法
定义异常处理方法,使用 @ExceptionHandler 标识可以处理的异常。
@ControllerAdvice
public class MyControllerAdvice {
@ExceptionHandler({NullPointerException.class,ArithmeticException.class})
public ModelAndView handlerException(Exception ex){
//如果出现了相关的异常,就会调用该方法
String msg = ex.getMessage();
ModelAndView modelAndView = new ModelAndView();
//把异常信息存入域中
modelAndView.addObject(“msg”,msg);
//跳转到error.jsp
modelAndView.setViewName(“/WEB-INF/page/error.jsp”);
return modelAndView;
}
}
③注入容器
可以使用注解注入也可以使用xml配置注入。这里使用注解注入的方式。在类上加 @Component 注解,注意要保证类能被组件扫描扫描到。
@ControllerAdvice
@Component
public class MyControllerAdvice {
//省略无关代码
}
2.3.1、@ControllerAdvice
使用注解实现异常分类管理
-
名称:
@ControllerAdvice
-
类 型: 类注解
-
位置:异常处理器类上方
-
作用:设置当前类为异常处理器类
-
范例:
@Component
@ControllerAdvice
public class MyControllerAdvice {
}
2.3.2、@ExceptionHandler
-
名称:
@ExceptionHandler
-
类型: 方法注解
-
位置:异常处理器类中针对指定异常进行处理的方法上方
-
作用:设置指定异常的处理方式
-
说明:处理器方法可以设定多个
-
范例:
@ExceptionHandler(Exception.class)
public String doOtherException(Exception ex){
return "出错啦,请联系管理员! ";
}
我们在实际项目中一般会选择使用@ControllerAdvice 来进行异常的统一处理。
因为如果在前后端不分离的项目中,异常处理一般是跳转到错误页面,让用户有个更好的体验。而前后端分离的项目中,异常处理一般是把异常信息封装到Json中写入响应体。无论是哪种情况,使用@ControllerAdvice的写法都能比较方便的实现。
例如下面这种方式就是前后端分离的异常处理方案,把异常信息封装到对象中,转换成json写入响应体。
@ControllerAdvice
@Component
public class MyControllerAdvice {
@ExceptionHandler({NullPointerException.class,ArithmeticException.class})
@ResponseBody
public Result handlerException(Exception ex){
Result result = new Result();
result.setMsg(ex.getMessage());
result.setCode(500);
return result;
}
}
========================================================================
-
Http协议规定了我们在进行文件上传时的请求格式要求。
-
表单项
type = "file"
-
表单的提交方式是
post
-
表单的
enctype
属性是多部份表单形式,及enctype = "multipart/form-data"
3.1.1、请求方式为POST请求
如果是使用表单进行提交的话,可以把form标签的method属性设置为POST。例如:
3.1.2、请求头Content-Type必须为multipart/form-data
如果是使用表单的话可以把表单的entype属性设置成multipart/form-data。例如:
SpringMVC使用commons-fileupload的包对文件上传进行了封装,我们只需要引入相关依赖和进行相应配置就可以很轻松的实现文件上传的功能。
3.2.1、导入依赖
commons-fileupload
commons-fileupload
1.4
3.2.2、配置文件上传解析器
SpringMVC容器中配置文件上传解析器
3.2.3、接收上传的文件数据并处理
上传表单
@Controller
public class UploadController {
@RequestMapping(“/upload”)
public String upload(MultipartFile uploadFile) throws IOException {
//文件存储 把上传上来的文件存储下来
uploadFile.transferTo(new File(“test.sql”));
return “/success.jsp”;
}
}
注意:方法参数名要和提交上来的表单参数名一致。
- 获取上传文件的原名
uploadFile.getOriginalFilename()
- 获取文件类型的MIME类型
uploadFile.getContentType()
- 获取上传文件的大小
uploadFile.getSize()
- 获取对应上传文件的输入流
uploadFile.getInputStream()
-
文件命名问题, 获取上传文件名,并解析文件名与扩展名
-
文件名过长问题
-
文件保存路径
-
重名问题
@RequestMapping(value = “/fileupload”)
//参数中定义MultipartFile参数,用于接收页面提交的type=file类型的表单,要求表单名称与参数名相同
public String fileupload(MultipartFile file,MultipartFile file1,MultipartFile file2, HttpServletRequest request) throws IOException {
//首先判断是否是空文件,也就是存储空间占用为0的文件
if(!file.isEmpty()){
//如果大小在范围要求内正常处理,否则抛出自定义异常告知用户(未实现)
//获取原始上传的文件名,可以作为当前文件的真实名称保存到数据库中备用
String fileName = file.getOriginalFilename();
//设置保存的路径
String realPath = request.getServletContext().getRealPath(“/images”);
//保存文件的方法,指定保存的位置和文件名即可,通常文件名使用随机生成策略产生,避免文件名冲突问题
file.transferTo(new File(realPath,file.getOriginalFilename()));
}
//测试一次性上传多个文件
if(!file1.isEmpty()){
String fileName = file1.getOriginalFilename();
//可以根据需要,对不同种类的文件做不同的存储路径的区分,修改对应的保存位置即可
String realPath = request.getServletContext().getRealPath(“/images”);
file1.transferTo(new File(realPath,file1.getOriginalFilename()));
}
if(!file2.isEmpty()){
String fileName = file2.getOriginalFilename();
String realPath = request.getServletContext().getRealPath(“/images”);
file2.transferTo(new File(realPath,file2.getOriginalFilename()));
}
return “page.jsp”;
}
========================================================================
如果我们想提供文件下载的功能。HTTP协议要求我们的必须满足如下规则。
①设置响应头Content-Type
要求把提供下载文件的MIME类型作为响应头Content-Type的值
②设置响应头Content-disposition
要求把文件名经过URL编码后的值写入响应头Content-disposition。但是要求符合以下格式,因为这样可以解决不同浏览器中文文件名 乱码问题。
Content-disposition: attachment; filename=%E4%B8%8B%E6%B5%B7%E5%81%9Aup%E4%B8%BB%E9%82%A3%E4%BA%9B%E5%B9%B4.txt;filename*=utf-8’'%E4%B8%8B%E6%B5%B7%E5%81%9Aup%E4%B8%BB%E9%82%A3%E4%BA%9B%E5%B9%B4.txt
③文件数据写入响应体中
我们可以使用之前封装的下载工具类实现文件下载
工具类代码:
import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.net.URLEncoder;
public class DownLoadUtils {
/**
-
该方法可以快速实现设置两个下载需要的响应头和把文件数据写入响应体
-
@param filePath 该文件的相对路径
-
@param context ServletContext对象
-
@param response
-
@throws Exception
*/
public static void downloadFile(String filePath, ServletContext context, HttpServletResponse response) throws Exception {
String realPath = context.getRealPath(filePath);
File file = new File(realPath);
String filename = file.getName();
FileInputStream fis = new FileInputStream(realPath);
String mimeType = context.getMimeType(filename);//获取文件的mime类型
response.setHeader(“content-type”,mimeType);
String fname= URLEncoder.encode(filename,“UTF-8”);
response.setHeader(“Content-disposition”,“attachment; filename=”+fname+“;”+“filename*=utf-8’'”+fname);
ServletOutputStream sos = response.getOutputStream();
byte[] buff = new byte[1024 * 8];
int len = 0;
while((len = fis.read(buff)) != -1){
sos.write(buff,0,len);
}
sos.close();
fis.close();
}
}
Handler方法定义
@Controller
public class DownLoadController {
@RequestMapping(“/download”)
public void download(HttpServletRequest request, HttpServletResponse response) throws Exception {
//文件下载
DownLoadUtils.downloadFile(“/WEB-INF/file/下海做UP主那些年.txt”,request.getServletContext(),response);
}
}
=================================================================================
因为我们有两种开发模式,我们分别来讲解两种模式在SpringMVC中的执行流程。
一种是类似JSP的开发流程:
把数据放入域对象中,然后进行页面跳转。
另外一种是前后端分离的开发模式,这也是目前市场上主流的模式:
把数据转化为json放入响应体中。
完整流程图如下:
1.用户发送请求至前端控制器DispatcherServlet。
2.DispatcherServlet收到请求调用HandlerMapping处理器映射器。DispatchServlet通过HandlerMapping根据具体的请求查找能处理这个请求的Handler。(HandlerMapping主要是处理请求和Handler方法的映射关系的)
3.HandlerMapping处理器映射器返回一个能够处理请求的执行链给DispatchServlet,这个链中除了包含Handler方法也包含拦截器。
4.DispatchServlet拿着执行链去找HandlerAdater执行链中的方法。
5.HandlerAdater会去执行对应的Handler方法,把数据处理转换成合适的类型然后作为方法参数传入
6.Handler方法执行完后的返回值会被HandlerAdapter转换成ModelAndView类型。(HandlerAdater主要进行Handler方法参数和返回值的处理。)
7.返回ModelAndView给DispatchServlet.
8.如果对于的ModelAndView对象不为null,则DispatchServlet把ModelAndView交给 ViewResolver 也就是视图解析器解析。
9.ViewResolver 也就是视图解析器把ModelAndView中的viewName转换成对应的View对象返回给DispatchServlet。(ViewResolver 主要负责把String类型的viewName转换成对应的View对象)
10.DispatchServlet使用View对象进行页面的展示。
前后端分离的开发模式中我们会使用@ResponseBody来把数据写入到响应体中。所以不需要进行页面的跳转。
故流程如下:
1.用户发送请求至前端控制器DispatcherServlet。
2.DispatcherServlet收到请求调用HandlerMapping处理器映射器。DispatchServlet通过HandlerMapping根据具体的请求查找能处理这个请求的Handler。(HandlerMapping主要是处理请求和Handler方法的映射关系的)
3.HandlerMapping处理器映射器返回一个能够处理请求的执行链给DispatchServlet,这个链中除了包含Handler方法也包含拦截器。
4.DispatchServlet拿着执行链去找HandlerAdater执行链中的方法。
5.HandlerAdater会去执行对应的Handler方法,把数据处理转换成合适的类型然后作为方法参数传入
6.Handler方法执行完后的返回值会被HandlerAdapter转换成ModelAndView类型。由于使用了@ResponseBody注解,返回的ModelAndView会为null ,并且HandlerAdapter会把方法返回值放到响应体中。(HandlerAdater主要进行Handler方法参数和返回值的处理。)
7.返回ModelAndView给DispatchServlet。
8.因为返回的ModelAndView为null,所以不用去解析视图解析和其后面的操作。