Spring MVC拦截器和全局异常处理(RESTful+状态码)

概述
  • 对于任何优秀的MVC框架,都会提供一些通用的操作,如请求数据的封装、类型转换、数据校验、解析上传的文件、防止表单的多次提交等。早期的MVC框架将这些操作都写死在核心控制器中,而这些常用的操作又不是所有的请求都需要实现的,这就导致了框架的灵活性不足,可扩展性降低。
  • Spring MVC中的拦截器 (Interceptor)类似于 Servlet 中的过滤器(Filter),主要用于拦截用户的请求并作相应的处理。例如:权限验证、记录请求信息的日志、判断用户是否登录等。
  • Spring MVC拦截器是可插拔式的设计,需要某一功能拦截器,只需在配置文件中应用该拦截器即可;如果不需要这个功能拦截器,只需在配置文件中取消应用该拦截器。

要使用拦截器需要对拦截器类进行定义和配置。有两种实现方式

  • 实现 HandlerInterceptor接口
  • 实现 WebRequestInterceptor接口【两个使用时一样的】

以第一种方式为练习。

第一步:实现HandlerInterceptor接口

该接口的位置:org.springframework.web.servlet包中,定义了三个 default 方法,它们都有方法体。自定义的拦截器类实现该接口的时候需要实现这三个方法。【注意实现方法的时候方法修饰符是 public, 粘贴过来记得改】

  • boolean preHandle()
    • 该方法在执行控制器方法之前执行。返回值为Boolean类型,如果返回false,表示拦截请求,不再向下执行,如果返回true,表示放行,程序继续向下执行(如果后面没有其他Interceptor,就会执行controller方法)。所以此方法可对请求进行判断,决定程序是否继续执行,或者进行一些初始化操作及对请求进行预处理。
  • void postHandle()
    • 该方法在执行控制器方法调用之后,且在返回ModelAndView之前执行。由于该方法会在DispatcherServlet进行返回视图渲染之前被调用,所以此方法多被用于处理返回的视图【处理完视图再去跳转】,可通过此方法对请求域中的模型和视图做进一步的修改。
  • afterCompletion
    • 该方法在执行完控制器之后执行,由于是在Controller方法执行完
    • 毕后执行该方法,所以该方法适合进行一些资源清理,记录日志信息等处理操作。
  • image-20211202145146778
第二步:配置拦截器

我的 ssm 项目的主体配置都是 springmvc.xml 中进行的,拦截器配置是有先后顺序的。

  • <mvc:interceptors>拦截器链标签
  • <mvc:interceptor>拦截器标签
  • <mvc:mapping path="/**/goods"/>配置拦截路径
    • /**:是根下的全部文件 包括子目录
    • /*:不包括子目录
  • <mvc:exclude-mapping path="/**/userinfo/login"/>:不拦截的路径,如果分不清楚可以在拦截全部的下面选出不拦截的
<!-- 配置拦截器链 -->
	<mvc:interceptors>
		<!-- 配置第一个拦截器 -->
		<mvc:interceptor>
			<!-- 拦截路径 -->
			<mvc:mapping path="/**/goods"/>
			<mvc:mapping path="/**/olders"/>
			<mvc:mapping path="/**/userinfo"/>
			<!-- 不拦截路径 -->
			<mvc:exclude-mapping path="/**/userinfo/login"/>
			<mvc:exclude-mapping path="/**/userinfo/doLogin"/>
			<mvc:exclude-mapping path="/**/login/login"/>
			<bean class="com.gpb.interceptors.LoginHandlerInterceptor"/>
		</mvc:interceptor>
        <!-- 后面还可以配置别的拦截器 它是有顺序的 -->
	</mvc:interceptors>
登录拦截
数据准备
  1. 用户实体:UserInfo

    public class UserInfo implements Serializable {
    	private static final long serialVersionUID = 1L;
    	private Integer id;
    	private String userName;
    	private String userPass;
        
    	public Integer getId() {
    		return id;
    	}
    	public void setId(Integer id) {
    		this.id = id;
    	}
    	public String getUserName() {
    		return userName;
    	}
    	public void setUserName(String userName) {
    		this.userName = userName;
    	}
    	public String getUserPass() {
    		return userPass;
    	}
    	public void setUserPass(String userPass) {
    		this.userPass = userPass;
    	}
    }
    
  2. 用户映射文件:UserInfoMapper接口和UserInfoMapper.xml映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.gpb.mapper.UserInfoMapper">
    	<cache />
    	<!-- 登录 -->
    	<select id="login" parameterType="java.lang.String" resultType="UserInfo">
    		select * from userinfo where username = #{userName}
    	</select>
    </mapper>
    
    public interface UserInfoMapper {
    	public UserInfo login(String userName);
    }
    
视图 jsp 准备
  • login文件夹下面的 login.jsp
  • 注意这些都是配置视图解析器之后在 WEB-INF/views下面的操作
  • el 表达式都是取的错误信息
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h3>用户登录</h3>${loginErr0}
<form action="${pageContext.request.contextPath}/userinfo/doLogin" method="post" >
	用户名:<input type="text" name="userName" value="${old.userName}">
	${loginErr}
	密码:<input type="password" name="userPass" value="${old.userPass}">
	${loginErr1}
	<input type="submit" value="登录">
</form>
</body>
</html>
业务层
  • 传输用户名到这 调用接口的方法去查询数据【条件是用户名】

  • 不顺便判断密码是为了在 controller 里面返回不同的错误信息回显

    @Service
    public class UserInfoService {
    	@Autowired
    	UserInfoMapper mapper = null;
    	
    	/**
    	 * 根据用户名判断用户是否存在
    	 * @param userName
    	 * @return
    	 */
    	public UserInfo login(String userName) {
    		UserInfo userInfo = mapper.login(userName);
    		if (userInfo!=null) {
    			return userInfo;
    		}
    		return null;
    	}
    }
    
控制层
  • 先整个跳转到 login.jsp的controller, WEB-INF 路径不可直接在外部访问

  • 尤其要注意最后跳转的路径即页面和映射的访问路径之间的区别

  • 把查询到的用户信息 user 放到 session 中,通过 session 保存

  • old 是页面输入传递过来的数据组合的实体 user 是数据库中存在的实体数据

    @Controller
    @RequestMapping("/userinfo")
    public class UserInfoController {
    	@Autowired
    	UserInfoService service = null;
    	
    	@RequestMapping("/login")
    	public String login() {
    		return "/login/login";
    	}
    	
    	@RequestMapping("/doLogin")
    	public String doLogin(Model model ,String userName, String userPass, HttpSession session) {
    		UserInfo user = service.login(userName);
    		UserInfo old = new UserInfo();
    		old.setUserName(userName);
    		old.setUserPass(userPass);
    		if (user != null) {
    			if (user.getUserPass().equals(userPass)) {
    				session.setAttribute("user", user);
    				return "index";
    			}
    			model.addAttribute("loginErr1", "用户密码错误");
    			model.addAttribute("old", old);
    			return "/login/login";
    		}
    		model.addAttribute("loginErr", "用户账号错误");
    		model.addAttribute("old", old);
    		return "/login/login";
    	}
    }
    
拦截器类
  • 拦截器包

  • 拦截器类:LoginHandlerInterceptor

  • 顺便可以打印测试顺序

  • session 取出存放的用户实体,为空则没有登陆,跳到登录页面,不为空则已经登录,可以正常使用,拦截器向下走

    public class LoginHandlerInterceptor implements HandlerInterceptor{
    	/**
    	 * 在controller方法之前调用
    	 */
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    			throws Exception {
    //		System.out.println("pre");
    		Object obj  = request.getSession().getAttribute("user");
    		if (obj == null) {
    			request.setAttribute("loginErr0", "您还没有登录");
    			request.getRequestDispatcher("/WEB-INF/views/login/login.jsp").forward(request, response);
    			return false;
    		}
    		return true;
    	}
    	
    	/**
    	 * 在跳转页面之前调用【业务执行完了】
    	 */
    	@Override
    	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
    			@Nullable ModelAndView modelAndView) throws Exception {
    		System.out.println("return"
    				+ "之前");
    	}
    	
    	/**
    	 * 在请求结束后
    	 */
    	@Override
    	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
    			@Nullable Exception ex) throws Exception {
    		System.out.println("controller执行结束之后");
    	}
    }
    
配置文件
  • 前面的配置文件
测试

RESTful支持

概念:RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP可以使用 XML 格式定义或 JSON 格式定义。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,使用JSON格式的REST风格的API具有简单、易读、易用的特点

资源:REST 是面向资源的,每个资源都有一个唯一的资源定位符(URI)。每个URI代表一种资源(resource),所以URI中不能有动词,只能有名词,而且所用的名词往往与数据库的表名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以URI中的名词也应该使用复数。

请求方式:

请求方式含义
GET(SELECT)从服务器取出资源(一项或多项)
POST(CREATE)在服务器新建一个资源
PUT(UPDATE)在服务器更新资源(更新完整资源)
PATCH(UPDATE)在服务器更新资源, PATCH更新个别属性
DELETE(DELETE)从服务器删除资源

【1】查询

查询	传统	REST	REST 后台接收
查询所有	http://localhost:8080/employee/list	http://localhost:8080/employees	@RequestMapping(value = “/employees”, method = RequestMethod.GET)
查询单个	http://localhost:8080/employee/list?id=1	http://localhost:8080/employees/1	@RequestMapping(value = “/employees/{id}”, method = RequestMethod.GET)
@ResponseBody
public Employee queryById(@PathVariable Long id) {}

【2】添加

添加	传统	REST	REST 后台接收
添加	http://localhost:8080/employee/add	http://localhost:8080/employees	@RequestMapping(value = “/employees”, method = RequestMethod.POST)
public Employee add(@ModelAttribute(“emp”) Employee employee) {}

【3】修改

修改	传统	REST	REST 后台接收
修改	http://localhost:8080/employee/update	http://localhost:8080/employees	@RequestMapping(value = “/employees”, method = RequestMethod.PUT)
public Employee update(@ModelAttribute(“emp”) Employee employee) {}

【4】删除

查询	传统	REST	REST 后台接收
删除	http://localhost:8080/employee/delete	http://localhost:8080//employees/{id}	@RequestMapping(value = “/employees/{id}”, method = RequestMethod.DELETE)
@ResponseBody
public JsonResult delete(@PathVariable Long id) {}

注意:
【1】当参数非常多的时候,不建议使用参数路径方式;
【2】如果参数名非常敏感,建议使用参数路径方式,可以隐藏参数名。

  • 相关注解

    @RestController@Controller + @ResponseBody组成(返回 JSON 数据格式)
    @PathVariable URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到控制器处理方法的形参中
    @RequestMapping 注解用于请求地址的解析,是最常用的一种注解
    @GetMapping 查询请求
    @PostMapping 添加请求
    @PutMapping 更新请求
    @DeleteMapping 删除请求
    @RequestParam 将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解)

    • @RequestParam(value=”参数名”,required=”true/false”,defaultValue=””)

      • value:参数名

      • required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错。

      • defaultValue:默认参数值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值

  • HTTP响应状态码

200 OK - [GET]	服务器成功返回用户请求的数据
  201 CREATED - [POST/PUT/PATCH]	用户新建或修改数据成功
  202 Accepted	表示一个请求已经进入后台排队(异步任务)
  204 NO CONTENT - [DELETE]	用户删除数据成功
  400 INVALID REQUEST - [POST/PUT/PATCH]	用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的
  401 Unauthorized - [*]	表示用户没有权限(令牌、用户名、密码错误)
  403 Forbidden - [*]	表示用户得到授权(与401错误相对),但是访问是被禁止的
  404 NOT FOUND - [*]	用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的
  405 Method Not Allowed	方法不允许,服务器没有该方法
  406 Not Acceptable - [GET]	用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)
  410 Gone -[GET]	用户请求的资源被永久删除,且不会再得到的
  422 Unprocesable entity - [POST/PUT/PATCH]	当创建一个对象时,发生一个验证错误
  500 INTERNAL SERVER ERROR - [*]	服务器发生错误,用户将无法判断发出的请求是否成功

Spring MVC 统一异常处理

全局处理 也有两种方式
  • 实现HandlerExceptionResolver接口,自定义异常处理器
  • 使用HandlerExceptionResolver接口的子类,也就是SpringMVC提供的异常处理器。
springmvc 提供的
  • DefaultHandlerExceptionResolver,默认的异常处理器。根据各个不同类型的异常,返回不同的异常视图。
  • SimpleMappingExceptionResolver,简单映射异常处理器。通过配置异常类和view的关系来解析异常。
  • ResponseStatusExceptionResolver,状态码异常处理器。解析带有@ResponseStatus注释类型的异常。
  • ExceptionHandlerExceptionResolver,注解形式的异常处理器。对@ExceptionHandler注解的方法进行异常解析。
自定义的
  • 自定义异常类

    package com.kafamiao.myexception;
    /**
     * 自定义的异常类
     * @author Administrator
     *
     */
    public class CustcomException extends RuntimeException{
    	private String message;
    	public CustcomException(String message) {
    		this.message = message;
    	}
    	public String getMessage() {
    		return message;
    	}
    	public void setMessage(String message) {
    		this.message = message;
    	}
    }
    
  • 实现 HandlerExceptionResolver接口定义一个异常处理器

    @Component
    public class CustcomExceptionResolver implements HandlerExceptionResolver{
    
    	@Override
    	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
    			Exception ex) {
    		//声明自定义异常
    		CustcomException custException = null;
    		if(ex!=null && ex instanceof CustcomException) {
    			custException = (CustcomException) ex;
    		}
    		else {
    			custException = new CustcomException(ex.getMessage());
    		}
    		//转向出错页面
    		ModelAndView modelAndView = new ModelAndView();
    		modelAndView.addObject("message", custException.getMessage());
    		modelAndView.setViewName("err/error");
    		return modelAndView;
    	}
    }
    
  • 测试

    	@RequestMapping("/findall")
    	public String findAll(Model model) {
    		if(0==0) {
    			throw new CustcomException("程序员主动抛出异常,查询数据出错了。");
    		}
    		//用业务类调用mapper
    		List<Goods> goodslist= service.findAll();
    		model.addAttribute("goodslist", goodslist);
    		return "goods/findall";
    	}
    
  • 异常现实页

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    出错原因:${message}
    </body>
    </html>
    
注解
  • @ExceptionHandler注解用来将一个方法标注为异常处理方法。该注解中只有一个可选的属性value,是一个Class<?>数组,用于指定该注解的方法所要处理的异常类,即所要匹配的异常。被该注解修饰的方法的返回值为异常处理后的跳转页面,其返回值可以是ModelAndView、String,或void;方法名随意,方法的参数可以是 Exception 及其子类对象、Model、HttpServletRequest、HttpServletResponse 等。系统会自动为这些方法参数赋值。

    @ExceptionHandler注解处理异常的作用域:单个类,只针对当前Controller。

    /**
     * 基于注解的异常处理
     */
    @Controller
    public class ExceptionController {
        @RequestMapping(value = "/exception1")
        public String exception1() {
            // 模拟出现异常
            System.out.println(10 / 0);
            return "success";
        }
    
        @RequestMapping(value = "/exception2")
        public void exception2() throws CustomException {
            // 模拟出现异常
            throw new CustomException("我抛出了一个异常!!!");
        }
    
        //处理自定义异常
        @ExceptionHandler({CustomException.class, ArithmeticException.class})
        public String exceptionHandler1(Exception e, Model model) {
            // 打印错误信息
            System.out.println(e.getMessage());
            e.printStackTrace();
            // 将错误数据存入请求域
            model.addAttribute("exception", e);
            return "show-annotation-message";
        }
    }
    
    • 注意:如果在Controller中单独使用这个注解是有缺陷的,就是不能够全局处理异常,因为进行异常处理的方法必须与出错的方法在同一个Controller里面,也就是说每个Controller类中都要写一遍,所以实用性不高。

    • 解决方案:可以将处理异常的信息抽取出来放在一个BaseController,然后对需要处理异常的Controller继承该类即可。

      public class BaseController {
          //处理自定义异常
          @ExceptionHandler({CustomException.class, ArithmeticException.class})
          public String exceptionHandler1(Exception e, Model model) {
              // 打印错误信息
              System.out.println(e.getMessage());
              e.printStackTrace();
              // 将错误数据存入请求域
              model.addAttribute("exception", e);
              return "show-annotation-message";
          }
      }
      
      • 但是还是存在同样的问题,每个类都得继承它,可见这种方式同样不可取,所以一般使用下面这种方式:@ControllerAdvice@ExceptionHandle 注解配合使用。
@ControllerAdvice+@ ExceptionHandler 注解(推荐)
  • @ExceptionHandler注解标注的异常处理方法必须与出错的方法在同一个Controller里面,所以这种方式是只对应单个Controller类。那么此时有一种更好的解决方案:可以使用@ControllerAdvice+@ExceptionHandler注解来解决,这个是 Spring 3.2 带来的新特性。

    ​ 两者一起使用的作用域:全局异常处理,针对全部Controller中的指定异常类

    /**
     * 基于注解的异常处理  @ControllerAdvice+@ExceptionHandler
     */
    // 这个注解表示当前类是一个异常映射类
    @ControllerAdvice
    public class MyException {
    
        // 在@ExceptionHandler注解中指定异常类型
        @ExceptionHandler(value = {CustomException.class, ArithmeticException.class})
        public ModelAndView exceptionMapping(Exception exception) {// 方法形参位置接收SpringMVC捕获到的异常对象
    
            // 可以将异常对象存入模型;将展示异常信息的视图设置为逻辑视图名称
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.addObject("exception", exception);
            modelAndView.setViewName("show-annotation-message");
            // 打印一下信息
            System.out.println(exception.getMessage());
            return modelAndView;
        }
    }
    
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值