了解RESTful
RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP,可以 使用XML格式定义或JSON格式定义。RESTFUL适用于移动互联网厂商作为业务接口的场景,实现第三方OTT调用移动网络资源的功能,动作类型为新增、变更、删除所调用资源。
RESTful的设计风格表现为将某些唯一的请求参数的值放在 URL中,使之成为URL的一部分,比如:http://localhost:8080/springmvc_war_exploded/user/100021
URL后面的一串数字就是RESTful的表现,这里的数字可以根据用户ID,替换为其他的用户ID 。RESTful只是一种设计风格,并不是一种规定,所以没有明确的执行方式,实际开发中,我们多会这么设计:
http://localhost:8080/springmvc_war_exploded/user/100021 查询ID为100021的用户信息
http://localhost:8080/springmvc_war_exploded/user/100021/delete 删除ID为100021的用户信息
RESTful建议根据希望获取数据的方式来选择请求方式,比如增加用post,删除用delete,修改用put,查询用get。而在实际开发中,我们多数只使用get和post,以查询为目的的URL用get,否则都用post。
在服务端,当涉及这样的URL时,需要使用占位参数{参数},使用@PathVariable注解请求参数, 可以将占位符的实际值注入到请求参数中!
@GetMapping("/{id}/info.page") public UserVO userInfo(@PathVariable Long id) { UserVO userVO = new UserVO(); userVO.setUsername("codingfire"); userVO.setPassword("123456"); return userVO; }
有时候,我不想把id这个参数暴漏给客户端,那么我就可以通过此注解来指定一个名字给客户端用,而我这里还是使用id。
@GetMapping("/{id}/info.page") public UserVO userInfo(@PathVariable("userID") Long id) { UserVO userVO = new UserVO(); userVO.setUsername("codingfire"); userVO.setPassword("123456"); return userVO; }
上传id的时候,这个占位符我希望必须时数字,数字之外的我就不处理,这种时候,可以通过正则表达式来进行处理:
@GetMapping("/{id:[0-9]+}/info.page") public UserVO userInfo(@PathVariable Long id) { UserVO userVO = new UserVO(); userVO.setUsername("codingfire"); userVO.setPassword("123456"); return userVO; }
还有一种情况,多种不冲突的正则表达式用在同一个URL上也是允许的,只要参数不一致就可以,看下怎么做:
@GetMapping("/{id:[0-9]+}/info.page") public UserVO userInfo(@PathVariable Long id) { UserVO userVO = new UserVO(); userVO.setUsername("codingfire"); userVO.setPassword("123456"); return userVO; } @GetMapping("/{username:[a-zA-Z]+}/info.page") public UserVO userInfo(@PathVariable String username) { UserVO userVO = new UserVO(); userVO.setUsername("codingfire"); userVO.setPassword("123456"); return userVO; }
以上,我们都是用了正则表达式,如果我不使用正则,会怎么样呢?先看看怎么写:
@GetMapping("/list/info.page") public UserVO userList() { ... }
当我们使用这个URL:/list/info.page时,就不会因为匹配到了/{id:[0-9]+}/info.page而不执行这个URL,这里明确是没有使用正则的,所以使用正则并不会影响到精确的URL匹配。
关于响应正文
先前我们已经说过了,响应正文可以是String类型,也可以是json类型,但在实际开发中,我们看到的响应正文都是比较规范的,比如响应码,响应信息,响应数据,我们可以通过抓包来看其他应用的响应格式,会发现,不同的公司,他们的响应格式几乎是一样的,这就是行业内不成文的规定,大家默认的一种通用方式。
下面我们来写下这个通用数据格式的类:
package cn.codingfire.springmvc.web; public class JsonResult<T> { //状态码 private Integer state; //消息,包括成功信息和失败信息 private String message; //返回数据,我们希望它可以转化为任何类型,所以一般会使用泛型 private T data; public Integer getState() { return state; } public void setState(Integer state) { this.state = state; } 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; } }
通用返回的类写好后,我们来用一下:
@GetMapping("/codingFireInfo.page") public JsonResult<UserVO> codingFireInfo() { UserVO userVO = new UserVO(); userVO.setUsername("codingfire"); userVO.setPassword("123456"); JsonResult jsonResult = new JsonResult(); jsonResult.setState(200); jsonResult.setMessage("请求成功"); jsonResult.setData(userVO); return jsonResult; }
接着直接重启Tomcat,在浏览器输入我们的URL,来来返回的结果,注意乱码问题,上面已经给出解决方案:
http://localhost:8080/springmvc_war_exploded/user/codingFireInfo.page
我们发现这么做是OK的。我们再来仔细看我们的代码,封装度其实并不高,我们希望状态码是枚举类型的,且我们可以直接使用成功失败的方法,不需要我们手写状态码和状态,因为这样很容易出错,这些值我们称之为魔法值,在阿里开发手册中,这种情况是严厉禁止的,因为会代码的可读性降低,所以还需要对这个类进行高度封装,这并不难,博主就不再给出封装部分的代码,大家可以自己手写一下,做个练习。
统一处理异常
异常在代码中还是比较经常出现的,基于面向对象语言的特点,我们希望异常可以统一处理,统一管理,Spring MVC恰好提供了这种方式,使我们在任何地方都可以直接抛出异常,交由统一的异常处理机制来处理,前提是你没有显示使用try...catch来捕获并处理异常。
处理异常有一种在当前类处理当前类异常的方式,直接添加方法如下:
@ExceptionHandler public String handleException(NullPointerException e) { return "NullPointerException!"; }
缺点是只能处理本类的异常,对于其他类的异常则鞭长莫及,这样就会造成重复写代码的情况,所以我们多会使用统一的异常处理机制,这种方式只做说明,知道就可以。
关于统一异常处理,需要自定义类来统一处理,我们来看看这个自定义类该怎么写:
package cn.codingfire.springmvc.exception; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public String handleException(NullPointerException e) { return "NullPointerException"; } @ExceptionHandler public String handleNumberFormatException(NumberFormatException e) { return "NumberFormatException"; } @ExceptionHandler public String handleThrowable(Throwable e) { e.printStackTrace(); return "Throwable"; } }
这样,处理异常的代码就可以放在专门的类中,在类上添加@ControllerAdvice注解,由于目前主流的响应方式都是“响应正文”的,所以可将@ControllerAdvice替换为 @RestControllerAdvice 。
不同的异常类型,可以使用不同的方法来单独处理,就像上面写的方式那样。一般我们会将异常的相关信息进行输出,这样可以方便开发者观察和分析问题。
了解这部分内容的同学会知道,@ExceptionHandler注解指定的异常类型优先级高于通过参数指定的异常类型的方式,而且此注解指定异常类型时,同一个方法也可以处理多种异常的情况,所以我们处理如下:
package cn.codingfire.springmvc.exception; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({NullPointerException.class, NullPointerException.class}) public String handleException(Throwable e) { return "Throwable"; } @ExceptionHandler(NumberFormatException.class) public String handleNumberFormatException(Throwable e) { return "NumberFormatException"; } @ExceptionHandler(Throwable.class) public String handleThrowable(Throwable e) { return "Throwable"; } }
Interceptor(拦截器)
Interceptor是拦截器,在Spring MVC框架中,拦截器可以在请求处理前后执行一些额外的代码,比如在请求用户信息之前判断用户登录状态来决定是否拒绝访问或者放行。虽然可以这么做,但拦截器的目的并不是拦截并阻止运行,其目的是处理多种不同请求的处理过程。多发生在不同的请求需要执行高度相似的代码,比如验证用户的登录状态。
使用拦截器和使用统一异常处理一样,需要使用自定义的类,并实现HandlerInterceptor接口,重写里面的三个方法,我们来看看这个类怎么写:
package cn.codingfire.springmvc.Interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //根据用户登录状态选择是否放行,不放行选择false return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //请求执行过程中 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //在请求执行结束后再统一执行某些操作 } }
拦截器写好了,但是却不能自动生效,因为拦截器都需要被注册才能生效,注册过程通过重写 WebMvcConfigure接口中的addInterceptors()方法即可,我们会到Spring MVC的配置类中看看该怎么注册:
package cn.codingfire.springmvc.config; import cn.codingfire.springmvc.Interceptor.LoginInterceptor; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @ComponentScan("cn.codingfire.springmvc") @EnableWebMvc public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/user/codingFireInfo.page"); } }
以上是匹配单个接口,但一般不会这么用,前面讲过,匹配的是某一类接口。
匹配用户模块所有接口:
//匹配user下所有接口 registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/user/*");
通配符的运用,可以匹配user下所有接口,addPathPatterns的参数也可以传数组,可以把要添加的URL放在数组中。
匹配多层级
//匹配user下所有接口 registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/user/**");
匹配多层级需要两个通配符。
不能匹配的情况
//匹配user下所有接口 registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/user/10021/info.page");
是不是刚刚讲过的RESTful风格?这里的数字是变化的,所以没法匹配。
能匹配路径,也能排除某些路径
registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/user/codingFireInfo.page") .excludePathPatterns("/user/register.page", "/user/login.page");
通过excludePathPatterns方法排除路径,路径可以是多个,也可以使用通配符,也可以给数组,和添加路径的用法一样。
结语
写到这里,Spring MVC框架就跟大家分享完了,看到这里没你已经可以写一个简单的Spring MVC框架的项目了。甚至一些小的项目,使用这个框架也完全够了,只要有一台服务器,它就可以正常工作,给客户端提供服务。下一篇,咱们SSM框架见。觉得不错就点个赞再走吧!