目录
一.前提了解
1.tomcat和servlet的关系?
2.springmvc想要实现web开发必须满足的条件是什么?
二.什么是SpringMVC
三.基于SpringMVC创建web项目
①创建项目并选择依赖
②设置热部署(部分代码改动不需要手动重新run即可生效)
四.理解前后端分离的开发过程
五.SpringMVC实现web开发
1.详解用户端返回的响应
@Controller
@ResponseBody
重定向和转发
转发
重定向
转发和重定向的区别:(M)
自定义返回类型
@RestController
@RequestMapping
2.详解服务端接收用户端的请求
2.1关于请求路径和请求头中的参数
@Pathvirable
使用postman对测试结果进行分析:
@RequestHeader
@CookieValue
@SessionAttribute
2.2关于请求参数
无注解的请求参数
@RequestParam
@RequestPart
@RequestBody
通过配置类设置编程式的配置方案
设置后端路径的统一前缀
设置拦截器
统一异常处理
统一的结果处理:
关于responseBodyAdvice的作用
一.前提了解
在我们真正的去讲述springmvc之前,我们先明确以下两个问题:
1.tomcat和servlet的关系?
我们都知道,servlet是实现动态页面的技术,servlet是进行web开发的规范,而tomcat是servlet容器,它在符合servlet规范的基础上对外提供了web服务器和端口号,实现对servlet的统一管理;同时可以接收用用户的请求,将请求发送给servlet,并在servlet给出响应之后将响应发送给客户端
2.springmvc想要实现web开发必须满足的条件是什么?
通过上面的论述,我们能清晰理解的是,tomcat满足了servlet规范,并在此基础上进行了web开发,springmvc如果想要实现web开发,也必须满足servlet规范。
二.什么是SpringMVC
SpringMVC是进行web开发的框架,它同样依托于springboot框架(springboot内包含了springmvc),受益于springboot框架中约定大于配置的原则,springboot内部默认配置了springmvc的文件路径,并对springmvc进行了很多默认的配置(不需要用户自己手动配置),springmvc内置了满足servlet规范的web服务器(相当于定制化的服务器)(对servlet进行了进一步的封装),而如果我们想要使用springmvc进行web开发,只需要满足springmvc的使用规范即可。
使用springmvc进行web开发的目的也很明了:使web开发更方便,提高web开发的效率
三.基于SpringMVC创建web项目
①创建项目并选择依赖
②设置热部署(部分代码改动不需要手动重新run即可生效)
③禁用JMX:因为如果不对其进行排除会导致在项目启动时报错,虽然这个报错不影响我们项目的实现,但是规范化起见,我们还是加上
④禁用tomcat,取而代之undertow(非必须选项,换是因为undertow的效率略高于tomcat)
⑤修改编码集
四.理解前后端分离的开发过程
springMVC 的MVC 是由以下三部分组成:①Model (模型)②View(视图) ③ Controller(控制器)
我们结合web请求和响应的过程来理解开发过程和springmc的三个组成部分:
用户发送http请求,通过controller层结合model层分析请求信息并给出响应响应,将响应的数据返回给view层,最后通过view层给出http响应 。
结合具体的放方法对此进行分析:
事实上专业的逻辑应该是这样的:
处理流程
DispatcherServlet 的处理流程可以分为以下几个步骤:接收客户端请求
当客户端发送请求时,DispatcherServlet 会接收并处理该请求。接收请求的方式取决于 DispatcherServlet 的配置,通常情况下,它会将请求映射到一个 URL,然后监听该 URL 的请求。创建请求对象
DispatcherServlet 会根据客户端请求创建一个请求对象,该对象中包含了客户端请求的所有信息,例如请求方法、请求头、请求参数等。处理请求映射
DispatcherServlet 会将请求映射到相应的控制器进行处理。请求映射是通过 HandlerMapping 进行的,HandlerMapping 负责将请求映射到一个或多个控制器,以便选择最合适的控制器进行处理。调用控制器
DispatcherServlet 会调用相应的控制器进行处理,控制器会根据请求参数和业务逻辑进行相应的处理,并返回一个 ModelAndView 对象。渲染视图
DispatcherServlet 会将 ModelAndView 对象传递给视图解析器(ViewResolver),视图解析器会根据 ModelAndView 中的视图名称来解析相应的视图对象。然后,DispatcherServlet 将模型数据传递给视图对象,以便渲染视图。最终,视图对象会生成相应的响应结果并返回给客户端。
五.SpringMVC实现web开发
1.详解用户端返回的响应
@Controller
@controller表示被这个注解修饰的类是一个bean(一般只能用来修饰类),并且这个bean负责处理web请求和响应
@ResponseBody
@ResponseBody表示响应正文,即规定返回值的类型(既可以修饰类,又可以修饰方法,修饰类代表所有的方法都设置返回值类型,修饰方法代表只有这个方法规定好返回值的类型),@ResponseBody规定被其修饰的方法的返回值以特定的数据格式进行返回,默认的返回形式是json
重定向和转发
如果没有使用@ResponseBody来规定好返回值类型的格式,的默认的返回值类型是String,并且代表一个路径:这个路径一般分为两种应用场景:转发和重定向
转发
语法格式:forward:/+路径
抓包查看特征:
①只有一次请求和响应 响应的资源是一个html页面
重定向
语法格式:"redirect:/+路径”
抓包查看特征:
第一次请求:
第一次响应:
第二次请求:
第二次响应:
转发和重定向的区别:(M)
1.重定向访问服务器两次,转发只访问服务器一次。
2.转发页面的URL不会改变,而重定向地址会改变
3.转发只能转发到自己的web应用内,重定向可以重定义到任意资源路径。
4.转发相当于服务器跳转,相当于方法调用,在执行当前文件的过程中转向执行目标文件,两个文件(当前文件和目标文件)属于同一次请求,前后页 共用一个request,可以通过此来传递一些数据或者session信息,request.setAttribute()和 request.getAttribute()。而重定向会产生一个新的request,不能共享request域信息与请求参数
5.由于转发相当于服务器内部方法调用,所以转发后面的代码仍然会执行(转发之后记得return);重定向代码执行之后是方法执行完成之后进行重定向操作,也就是访问第二个请求,如果是方法的最后一行进行重定向那就会马上进行重定向(重定向也需要return)。
自定义返回类型
除了使用@ResponseBody设置返回值类型和进行转发和重定向之外,还能实现自定义的返回值类型,操作如下:
我们举一个例子来进行说明:我们传输给客户端一个.doc对象(网络资源下载)
1.创建文件路径对象
2.读取路径中的字节数组
3.设置自定义的返回对象
@Controller
public class SelfController {
@GetMapping("/object1")
public ResponseEntity test() throws IOException {
//创建返回值
//传输字节码文件
Path p=new File("D:\\大物实验报告\\42109211014_20221018142451.doc").toPath();
byte[]bytes= Files.readAllBytes(p);
return ResponseEntity.ok().header("content-type", "application/msword").body(bytes);
}
}
我们使用路径进行访问
@RestController
我们暂且可以将其作用简单理解为:@Controller和@Responsebody的组合注解
使用方法:加载类前面
@RequestMapping
@RequestMapping代表请求路径,该注解既可以加到类上,也能加到方法上如果类上和方法上同时加了这个注解,则请求路径表示为:类路径+方法路径
使用方法:
部分属性:
●value:定义request请求的映射地址
●method:定义地request址请求的方式,包括【GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.】默认接受get请求,如果请求方式和定义的方式不一样则请求无法成功。
●params:定义request请求中必须包含的参数值。
2.详解服务端接收用户端的请求
2.1关于请求路径和请求头中的参数
@Pathvirable
使用方式:用于修饰形参
@Pathvirable 作用:标识请求路径中的动态参数(从使用@RequestMapping中请求路径中读取被@Pathvirable 修饰的方法形参变量名一致的变量,将其读取至方法中相同变量名的形参变量中。)
代码演示:
@RestController
public class PathTest {
@RequestMapping("/test/{id}")
public Object test(@PathVariable Long id){
//创建容器
HashMap<String, Long> stringLongHashMap = new HashMap<>();
stringLongHashMap.put("id", id);
return stringLongHashMap;
}
使用postman对测试结果进行分析:
@pathvirable修饰的变量类型可以对请求路径传来的变量进行校验,如果请求路径中的参数和参数的类型不符,会报400错误
@RequestHeader
作用:通过绑定请求头中的字段名,获取请求头的字段信息
使用示例:
@RequestMapping("/header") public Object getHeader(@RequestHeader("user-agent") String headers){ HashMap<String, String> stringLongHashMap = new HashMap<>(); stringLongHashMap.put("user-agent", headers); return stringLongHashMap; }
@CookieValue
作用:通过绑定cookie中的键名,获取cookie中的信息
使用方式:
@RequestMapping("/cookie") public Object getCookie(@CookieValue("JSESSIONID") String cookie){ HashMap<String, String> stringLongHashMap = new HashMap<>(); stringLongHashMap.put("my-cookie", cookie); return stringLongHashMap;
@SessionAttribute
在讲解这个之前,我们先简要说一下cookie和session之间的关系:session保存在服务端,服务端用它来保存用户信息,我们可以将session理解为键值对,键为随机生成的sessionId,值为当前用户的session对象;cookie保存到客户端,也是用来保存用户信息,再校验用户信息时根据sessionId在服务端匹配对应的sessoin对象,进行身份校验
@SessionAttribute用来处获取请求参数中的session信息(根据sessionId在服务端获取session对象,之后进行校验用户信息)
使用实例:
@RequestMapping("/login") public Object info(HttpServletRequest request){ //通过session存储session信息 HttpSession session = request.getSession(true); //存储session信息 session.setAttribute("user", "zhangsan"); //创建容器并返回 HashMap<String, String> map = new HashMap<>(); map.put("user", "zhangsan"); return map; } @RequestMapping("/check") public Object checkLogin(@SessionAttribute("user") String name){ HashMap<String, String> map = new HashMap<>(); map.put("user", name); return map; }
一般在进行登录校验时的大体流程如下:
首次登录时检查session对象是否存在(不存在则新创建一个session对象),之后根据前端传来的用户信息对其进行存储,返回响应,之后如果再进行其他操作(需要在用户登录的基础上)则需要根据sessionId从服务器中获取session对象进行校验
结果分析:
2.2关于请求参数
无注解的请求参数
前端的请求参数通常分为四种数据格式进行传输:query-string ,表单类型的数据、json格式的数据和form-data类型的数据,而我们后端接收请求的java对象,我们将其分为简单数据类型(基本数据类型+包装类+String)和复杂数据类型(集合框架和自定义的类型),以下我们将从这两种数据类型进行分析;
我们先给出结论:无论是简单数据类型还是复杂数据类型,在没有注解的情况下后端能进行数据解析的只有query-string ,表单类型的数据、json格的数据,json类型的数据需要使用注解(@RequestBody),我们根据这个画一个表格对此进行说明:我们分别对这几种前端数据类型进行分析:
@RequestParam
@RequestParam(只能用来修饰简单类型的形参)
参数:vlaue/name:用来标识请求参数的名称(请求路径中请求参数的名称),这个参数也可以不传,只要保证方法中的参数和请求路径参数完全一致即可
required:这个参数在请求参数中是否必须提供:FALSE代表不是必须提供,TRUE代表必须提供
参数类型类型不匹配或者要求必须提供的参数没有提供会报400错误
作用:将请求参数转化为控制器方法的形参
使用实例:
@RequestMapping("/requestB") public Object res(@RequestParam String name ,@RequestParam() Integer id){ HashMap<String, String> map = new HashMap<>(); map.put("name", name); map.put("id", id+""); return map; }
@RequestPart
@RequestPart一般用于用于将
multipart/form-data
类型数据映射到控制器处理方法的参数中注解解析
① value:
绑定的参数名称,参数值为String类型。
② name:
绑定的参数名称,参数值为String类型。name和value可以同时使用,但两者的值需一致,否则会出现错误。(400)
③ required:请求头中是否必须包含指定的值,默认值为true。
required为true时,如果请求头中缺少指定的值,则会抛出异常。
required为false时,如果请求头中缺少指定的值,则会返回null。
使用用例:
@RequestMapping("/upload") //上传文件 public Object upload(@RequestPart MultipartFile head, User user) throws IOException { head.transferTo(new File("D:/上传的"+head.getOriginalFilename())); HashMap<String, Object> map = new HashMap<>(); map.put("file", head); return map; }
文件上传可能出现的错误:系统找不到指定文件
java.io.FileNotFoundException: C:\Users\86131\AppData\Local\Temp\tomcat.8080.6906634590984434583\work\Tomcat\localhost\ROOT\upload_135c4883_3414_49af_b54e_39dbe063b0de_00000002.tmp (系统找不到指定的文件。)我们去系统指定的目录中查看文件是否上传成功:
发现虽然报了错误,但是文件上传成功了:
在分析错误原因前我们先对文件上传的逻辑进行分析:客户端向服务端传输文件,文件首先报错到系统网卡然后部分文件信息保存到服务端内存,而有关文件内容将会在本地的一个临时目录中进行保存,如果我们调用transferTo(),会将默认保存的系统文件移动到我们指定的文件目录中,这种情况下原来临时目录的文件就没有了,所以当我们后面再调用有关文件信息的方法,首先在服务端的内存中进行查找,如果服务端内存没有这个信息,则需要到临时目录中进行查找,如果临时目录中没有这个文件,则会报以上的错误。
如何解决这个问题呢?
解决思路也比较清晰:在调用文件相关信息的时候,在transferTo()方法执行之前进行调用即可。
@RequestBody
我们前面有说到的是无论是无注解的请求参数还是使用@RequestParam注解,都不支持对json数据的解析,而@RequestBody能够实现对json数据的解析,不支持对其他数据类型的解析。
使用实例:
@RequestMapping("/json") public Object jsonSend( @RequestBody User user){ HashMap<String, String> map = new HashMap<>(); map.put("name", user.getName()); map.put("id", user.getId()+""); return map; }
通过配置类设置编程式的配置方案
我们知道可以通过设置配置文件的格式对springboot项目进行设置,但是通过配置文件设置比较麻烦或者设置通过配置文件无法实现的我们可以通过编程(设置配置类)来设置配置。通过@Configuration来实现这个功能:被@Configuration修饰的类会在springboot项目启动时进行加载配置,我们通过使用以下两个实例进行加载说明:
设置后端路径的统一前缀
功能:为所有的后端控制器添加统一的前缀(名为:api)
应用场景:在访问后端的请求路径时需要对其的部分路径的处理逻辑加一些统一的校验,为了区分前端路径和后端路径,我们统一为所有的控制器都加一个前缀.
我们给出代码实例:
因为我们进行的是有关web开发的配置,所以我们进行的配置要继承WebMvcConfigurer的接口
@Configuration
public class AppConfig implements WebMvcConfigurer {
//调用路径匹配的api,为所有的后端逻辑添加前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c->{
//通过循环遍历所有的后端controller判断是否要加前缀(暂时设置所有的路径都为前缀)
//如果需要在此处使某些控制器不加前缀,在此处应该加一些别的逻辑经即可
return true;
});
}
我们在实际的路径中并没有api请求路径, 但是我们进行访问时必须通过“api”路径进行访问,否则直接报404
设置拦截器
我们首先了解一下web开发的三大组件:servlet(连接器)、listener(监听器)和filter(过滤器),我们需要明确的是:spring官方并没有提供拦截器,拦截器是springmvc提供的,但是其实现原理是和过滤器类似。
拦截器的处理逻辑如下:
在客户端返送请求之前会通过拦截器preHander相关方法进行处理,preHander返回一个Boolean值,TRUE则继续向下执行,FALSE则直接返回;在controller层返回响应后同样也会通过拦截器,这时回调用postHander()方法,最后将响应返回给客户端,我们自定义的拦截器通常会重写以下三个方法:
1、preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) 方法在请求处理之前被调用。该方法在 Interceptor 类中最先执行,用来进行一些前置初始化操作或是对当前请求做预处理,也可以进行一些判断来决定请求是否要继续进行下去。该方法的返回至是 Boolean 类型,当它返回 false 时,表示请求结束,后续的 Interceptor 和 Controller 都不会再执行;当它返回为 true 时会继续调用下一个 Interceptor 的 preHandle 方法,如果已经是最后一个 Interceptor 的时候就会调用当前请求的 Controller 方法。
2、postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) 方法在当前请求处理完成之后,也就是 Controller 方法调用之后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作。
3、afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) 方法需要在当前对应的 Interceptor 类的 preHandle 方法返回值为 true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。此方法主要用来进行资源清理。
我们设置拦截器的过程可以从以下三个方面进行设计分析:①自定义拦截器②设置拦截路径③设置拦截器的处理逻辑
我们给出一个拦截器中以下的三种代码:
①路径处理:配置拦截路径支持模糊匹配:而进行配置的方式,一般是通过添加拦截路径和排除一些不拦截的路径来处理
/**:添加任意路径
/api/**:添加api下的任意路径
/api/*:添加api下的一层目录
设置排除路径:
/api/login:排除api下的login的目录
/api/register:排除api下的register目录
②自定义拦截器:
自定义拦截器并引用HandlerInterceptor接口,然后重写接口中的方法
③实现方法逻辑
在重写的方法中实现方法逻辑
完整代码:
@Configuration public class AppConfig implements WebMvcConfigurer { //调用路径匹配的api,为所有的后端逻辑添加前缀 @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.addPathPrefix("api", c->{ //通过循环遍历所有的后端controller判断是否要加前缀(暂时设置所有的路径都为前缀) return true; }); } @Override public void addInterceptors(InterceptorRegistry registry) { //使用注册器进行注册过滤器 registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/api/**").excludePathPatterns("/api/login") .excludePathPatterns("/api/register"); } }
public class LoginInterceptor implements HandlerInterceptor { //设置请求逻辑:在请求前进行处理 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取session HttpSession session = request.getSession(false); if(session!=null){ String user = (String)session.getAttribute("user"); if(user!=null){ //判断是否是用户 if(user.equals("admin")){ return true; } } } response.setStatus(401); return false; } }
Interceptor 作用
日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
权限检查:如登录检测,进入处理器检测用户是否登录;
性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录)
通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。
统一异常处理
如果我们通过浏览器进行请求时出现错误,极有可能把后端的报错信息全部显示到浏览器上,一方面这样存在极大的安全隐患(其他程序员根据报错可能会对我们后端的实现有一定的了解,从而进行破坏性操作),另一方面对用户的体验也并不是很友好。
因此我们需要进行统一的异常处理,将后端报错进行统一起来,我们进行如下操作:使用@ControllerAdvice注解进行控制器增强,使用@ExceptionHandle(异常类)进行统一的异常处理,@ExceptionHandle(异常类)的处理逻辑如下:根据括号中的异常类信息,当发生括号内的异常类中的报错信息之后,使用其下的方法进行报错的处理(起到的作用是异常处理的catch)
@ControllerAdvice
public class ExceptionHandle {
//默认以json形式返回数据
@ResponseBody
//统一捕获异常类
@ExceptionHandler(Exception.class)
public Object handle(Exception e){
HashMap<String, Object> map = new HashMap<>();
map.put("code",500);
map.put("message",e.getMessage());
return map;
}
需要注意的是:@ExceptionHandle(异常类)只能捕获其括号内的异常,一旦出了其括号内异常类的范围,还是会直接将错误信息直接返回给客户端:修改之后的错误信息如下:
@ControllerAdvice
public class ExceptionHandle {
//默认以json形式返回数据
// @ResponseBody
// //统一捕获异常类
// @ExceptionHandler(Exception.class)
// public Object handle(Exception e){
// HashMap<String, Object> map = new HashMap<>();
// map.put("code",500);
// map.put("message",e.getMessage());
// return map;
// }
@ResponseBody
//统一捕获异常类
@ExceptionHandler(IOException.class)
public Object handleIo(Exception e){
HashMap<String, Object> map = new HashMap<>();
map.put("code",500);
map.put("message",e.getMessage());
return map;
}
统一的结果处理:
①自定义结果类
@Data
public class Result {
private Boolean ok;
private Object data;
private String error;
}
②使用@ControllerAdvice统一处理结果:设置ok data(data在返回的body中直接处理) 和error
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//进行赋值处理
Result result = new Result();
result.setOk(true);
result.setData(body);
result.setError(null);
return result;
}
}
我们使用这个结果类访问以下代码:
import java.util.HashMap;
/**
* @author tongchen
* @create 2023-04-28 10:34
*/
@RestController
public class UserController {
@RequestMapping("/user/{id}/{name}")
public Object user(@PathVariable String name,@PathVariable Integer id ){
HashMap<String, Object> map = new HashMap<>();
map.put("user", name);
return map;
}
}
结果如下:
但是我们发现一个问题,在这个代码中,我们将返回信息除了data外直接写死了 ,我们思考一个问题:在真正的业务场景中,我们应该如何统一处理返回结果呢?(异常结果处理和统一结果处理)
A:修改返回类,取消统一设置返回方法,在具体的业务场景中具体返回结果,异常类单独返回结果
具体code及结果如下:
@Data
public class Result<T> {
private Integer ok;//1代表成功 0代表失败
private T data;
private String msg;//正确或异常的信息
public static <T>Result<T> success(T data){
Result<T> result = new Result<>();
result.setData(data);
result.setOk(1);
return result;
}
public static <T>Result<T>error(String msg){
Result<T> result = new Result<>();
result.setOk(0);
result.setMsg(msg);
return result;
}
}
正常的结果类:
@RestController
public class UserController {
@RequestMapping("/user/{id}/{name}")
public Object user(@PathVariable String name,@PathVariable Integer id ){
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name",name);
return Result.success(map);
}
}
异常的结果类:
public class MyException extends Exception{
public MyException(String message) {
super(message);
}
}
@RestController
public class ExceptionController {
@RequestMapping("/test")
public void test() throws MyException {
throw new MyException("HAHAHH");
}
}
异常的结果类处理:
@ControllerAdvice
public class ExceptionHandle {
//默认以json形式返回数据
// @ResponseBody
// //统一捕获异常类
// @ExceptionHandler(Exception.class)
// public Object handle(Exception e){
// HashMap<String, Object> map = new HashMap<>();
// map.put("code",500);
// map.put("message",e.getMessage());
// return map;
// }
@ResponseBody
//统一捕获异常类
@ExceptionHandler(MyException.class)
public Object handleIo(Exception e){
HashMap<String, Object> map = new HashMap<>();
return Result.error("服务器错误");
}
}
正常结果:
异常结果:
关于responseBodyAdvice的作用
ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行的对 response 的一些处理,可以实现对 response 数据的一些统一封装或者加密等操作。
该接口一共有两个方法:
(1)supports —— 判断是否要执行beforeBodyWrite方法,true为执行,false不执行 —— 通过supports方法,我们可以选择哪些类或哪些方法要对response进行处理,其余的则不处理。
(2)beforeBodyWrite —— 对 response 处理的具体执行方法。