SpringMVC
学习 SpringMVC 时的 GitHub 地址:https://github.com/2022zhang125/SpringMVC.git
主要分享一些关于 SpringMVC 自己的见解,如若有错,欢迎指正。
在学习 SpringMVC 时,我采用了 Thymeleaf 作为模版语言的 All In One 形式学习的。
前端分发器
- 通过 DispatcherServlet 前端分发器将请求进行分发操作
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!--前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--配置初始化参数,让SpringMVC在classpath中-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</init-param>
<!--让DispatchServlet在服务器启动时自动创建-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
请求方式(@RequestMapping)
后期可以直接使用 PostMapping 这些代替掉。
-
RESTFul 风格的请求格式
-
所谓 RESTFull 风格,就是将传统的请求
/springmvc/login?username=zhangsan&password=123,通过对请求方式来判断执行的操也就可以写成
/springmvc/login/zhangsan/123就请求的值作为 URL 进行发送。
-
-
举例,路径中通过
{变量名}然后通过@PathVariable 来进行接收操作-
@RequestMapping("/login/{username}/{b}")
public String testRESTFul(@PathVariable("username") String username, @PathVariable("b") String password){
System.out.println("账号:" + username + ",密码:" + password);
return "ok";
}
提示1.占位符使用的是 `{}`
2.接收参数使用 `@PathVariable("username")......`
3.其中`@PathVariable` 后要与占位符一致!!! -
-
RESTFul 格式的请求准则
请求方式 对应操作 POST 增加 PUT 修改 GET 获取 DELETE 删除 HEAD 获取响应头
后端获取前端数据流程和方式
后端流程
-
用户直接访问
-
开启服务,
DispatcherServlet持续监听 / (也就是除了 JSP 之外的所有请求) -
分发请求到指定的方法上,比如是:/ 请求
-
通过拼接前后缀使
逻辑视图变为物理视图 -
通过 Thymeleaf 的视图解析器将页面展示
-
-
用户点击超链接
- 用户点击超链接后进行跳转
- 被
DispatcherServlet检测/user/register - 执行
register方法 ----> 拼接 ---->展示。
-
用户表单提交
- 用户填写完表单后,点击提交
- 被
DispatcherServlet检测/user/register /user/register01 - 执行
toRegister方法,通过HttpServletRequest获取表单数据。
后端获取数据方式
-
使用
@RequestParam注解,将请求映射到我们的变量上-
@RequestMapping(value = "/user/register02",method = RequestMethod.POST)
public String toRegister02(@RequestParam("username") String username, @RequestParam("password") String password){
// 使用@RequestParam将这个的value值映射上我们的变量username。
System.out.println("用户名:" + username + ",密码:" + password);
return "ok";
}
-
-
使用
POJO类作为接受参数
用一个 POJO 对象,该对象存储着此次请求的所有变量,然后让请求映射到我们的 POJO 上即可。
@PostMapping("/user/register02")
public String toRegister02(User user){
System.out.println(user);
return "ok";
}
通过设置字符编码过滤器解决 POST 请求乱码问题
<!--使用SpringMVC自带的编码过滤器来定义请求和响应的字符编码方式-->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<!--设置编码方式-->
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<!--设置请求必须要使用我们的UTF-8-->
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<!--设置响应必须要使用我们的UTF-8-->
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<!--对所有的请求都过滤-->
<url-pattern>/*</url-pattern>
</filter-mapping>
其实就是配置一个特殊的类,然后设置其属性值。
SpringMVC 对三个域的操作
域操作,一般是能选择小的就选小的,这样会优化空间。
Request 请求域
生命周期:一次请求
-
获取请求域的几种方式
-
原生
ServletAPI-
@RequestMapping("/requestServletAPI")
public String requestServletApi(HttpServletRequest request){
// 存放数据
request.setAttribute("requestScope","使用SpringMVC中的原生Servlet API实现一次请求的请求域数据共享");
return "ok"; // 这里默认是使用转发机制(forward)
}
-
-
Map接口-
@RequestMapping("/requestScopeMap")
public String requestScopeMap(Map<String, Object> map){
map.put("requestScope","使用SpringMVC中的Map接口实现一次请求的请求域数据共享");
return "ok";
}
-
-
Model接口-
@RequestMapping("/requestScopeModel")
public String requestScopeModel(Model model){
model.addAttribute("requestScope","使用SpringMVC中的Model接口实现一次请求的请求域数据共享");
return "ok";
}
-
-
ModelMap类-
@RequestMapping("/requestScopeModelMap")
public String requestModelMap(ModelMap modelMap){
modelMap.put("requestScope","使用SpringMVC中的ModelMap接口实现一次请求的请求域数据共享");
return "ok";
}w
-
-
ModelAndView类-
@RequestMapping("/requestModelAndView")
public ModelAndView requestModelAndView(){
ModelAndView mav = new ModelAndView();
mav.addObject("requestScope","使用SpringMVC中的ModelAndView类实现一次请求的请求域数据共享");
// 跳转的视图
mav.setViewName("ok");
return mav;
}
-
-
只要是请求,在经过Dispatcher分发器时,都会被封装成一个ModelAndView对象。(适配器模型)
Session 会话域
生命周期:一次浏览器的开启与页面的跳转,只要浏览器不关 Session 不清除,我就在
-
通过原生 HttpSession 来获取
-
@RequestMapping("/sessionServletAPI")
public String sessionServletApi(HttpSession session){
session.setAttribute("sessionScope","在SpringMVC中使用原生Servlet API获取Session域对象并统一会话域数据");
return "ok";
}
-
-
使用
ModelMap类 +@SessionAttributes({"sessionScope"})来指定某个 Key 储存到 Session 内-
@SessionAttributes({"sessionScope"})
@Controller
public class SessionScopeTestController {
@RequestMapping("/sessionModelMap")
public String sessionModelMap(ModelMap modelMap){
modelMap.put("sessionScope","在SpringMVC中使用ModelMap接口获取Session域对象并统一会话域数据");
return "ok";
}
}
-
Application 应用域
生命周期:整个应用各个地方都可以调用,一般是公共元素
获取ServletContext对象的方式(用得少),所以直接用ServletAPI原生的即可
@RequestMapping("/applicationServletAPI")
public String applicationServletAPI(HttpServletRequest request){
ServletContext context = request.getServletContext();// 这里不能直接将 ServletContext作为参数进行获取,需要通过request域、session域来间接获取。
context.setAttribute("applicationScope","使用原生Servlet API实现应用域数据共享");
return "ok";
}
request ---> ServletContext ---> Application 对象
转发与重定向方式
改变 return 的格式,将其设定为转发(默认)/重定向
转发:return "forward:/b"
重定向:return "redirect:/b" (使用的较多)
如果对于一个只是跳转,而没有任何业务的Controller时,我们可以采用 mvc:view-controller去指定 路径与资源的映射关系,从而简化代码
但是,在springmvc.xml文件中配置时,会导致所有的注解失效,于是,我们需要绑定上 mvc-annotation-driven/> 一起使用。
<mvc:view-controller path="/" view-name="index" />
<mvc:annotation-driven/>
访问服务器静态资源
-
打开 web 容器自带的 defaultServlet 这个 Servlet 是当我们的 DispatcherServlet 为 404 时,会使用这个 Servlet 去静态资源里面找。
-
配置静态资源处理
<mvc:resources mapping="/static/**" location="/static/" /> -
注意:静态资源一定要放在 webapp 下,不能放在 WEB-INF 下
RESTful 风格的编程
这里着重写一下对于前端,非 GET,POST 请求的其他请求,如何发送?
这里以PUT请求为例
-
第一步:前提是在 POST 请求之下,就是必须得是 POST 请求
-
第二步:添加隐藏域
-
<input type="hidden" name="_method" value="put" />
-
-
第三步:在 Web.xml 文件中开启过滤器,用于将 POST 请求转为对应的隐藏域请求
-
<!--添加HiddenHttpMethodFilter用于将POST请求转化为PUT、delete请求-->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
-
如果,我们将HiddenHttpMethodFilter 配置在 CharacterEncodingFilter 之上,则会率先调用前者
这样就会导致 后者无法执行原有操作
因为,request.setCharacterEncoding("UTF-8") 的前面不能有 request.getParamter("")操作,这样会导致编码设置出错。
因此,我们需要交换位置。
所以,隐藏过滤器必须要在字符编码过滤器之后配置!!!.
@ResponseBody 与@RequestBody 的使用方法
-
对于
@RequestBody,这是请求体注解,自然而然应该位于方法的形参内,用于将前端的请求体内的内容原封不动的变为 String 传递给后端,用的还是 FormHttpMessageConverter 转换器。-
public void save(@RequestBody String requestStr){
System.out.println(requestStr); // key=value&key=value&key=value......
}
-
-
对于
@ResponseBody,属于响应体注解,让 return 的结果不在作为逻辑视图名称进行拼接操作,而是直接作为 HTML 正文返回给前端页面进行展示操作。使用的 HttpMessageConverter 是 StringHttpMessageConverter
JSON 与 POJO 对象互相转换
-
首先,引入 JackSon 的依赖
-
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
-
-
POJO 转 JSON,直接转换即可
-
@GetMapping("/ajax")
@ResponseBody
public User ajax(){
User user = new User(123,"张三","321");
return user;
}提示注意!这里使用的消息转换器为:**MappingJackson2HttpMessageConverter**
-
-
JOSN 转 POJO 对象,需要使用@RequestBody 注解
-
前端
-
let jsonObj = {"name" : "lisi", "password" : '321'}
Vue.createApp({
data(){
return{
message : ''
}
},
methods : {
async getMessage(){
let response = await axios.post([[@{/}]] + 'save',JSON.stringify(jsonObj),{
headers:{
"Content-Type" : "application/json"
}
})
this.message = response.data
}
}
}).mount("#app")
-
-
后端
-
@Controller
public class RequestBodyController {
@ResponseBody
@PostMapping("save")
public String save(@RequestBody User user){
return user;
}
}
// 输出是:User{id=null, name='lisi', password='321'},说明获取到了前端的JSONObj对象并且将其转为了User对象,前提是引入jackson-databind依赖,这里使用的Http消息转换器为:MappingJackson2HttpMessageConverter
-
-
上传与下载
代码都是死的,这些基本就写成工具类就行,要的时候调用即可。
-
前端
-
<form th:action="@{/upload}" enctype="multipart/form-data" method="post">
文件上传:<input type="file" name="fileName" /> <br />
<input type="submit" value="上传" />
</form>
<br />
-
编写前端页面,注意表单必须为POST请求,而且enctype必须为:multipart/form-data 而不是:application/x-www-form-urlencoded
-
后端 XML 文件配置
-
<multipart-config>
<!--单个文件最大大小-->
<max-file-size>102400</max-file-size>
<!--表单上所有文件大小-->
<max-request-size>102400</max-request-size>
<!--设置最小上传文件大小-->
<file-size-threshold>0</file-size-threshold>
</multipart-config>
-
-
后端完整的上传代码
-
@Controller
public class FileController {
@ResponseBody
@PostMapping("/upload")
public String fileup(@RequestParam("fileName") MultipartFile multipartFile, HttpServletRequest request) throws IOException {
String originalFilename = multipartFile.getOriginalFilename();
BufferedInputStream bis = new BufferedInputStream(multipartFile.getInputStream());
ServletContext application = request.getServletContext();
String realPath = application.getRealPath("/upload");
File file = new File(realPath);
if (!file.exists()) {
file.mkdirs();
}
File destFile = new File(file.getAbsolutePath() + "/" + UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf('.')));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
byte[] bytes = new byte[1024 * 100];
int readCount = 0;
if ((readCount = bis.read(bytes)) != -1) {
bos.write(bytes, 0, readCount);
}
bos.flush();
bos.close();
bis.close();
return "ok";
}
}
-
-
后端完整的下载代码
-
public class FileController{
@GetMapping("/download")
public ResponseEntity<byte[]> fileDownload(HttpServletRequest request) throws IOException {
File file = new File(request.getServletContext().getRealPath("/upload") + "/83b976d1-b07a-4ba5-8dac-02ddbf9d620f.jpg");
// 响应头的设置
HttpHeaders headers = new HttpHeaders();
// 设置响应类型
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 设置下载文件名称(固定写法)
headers.setContentDispositionFormData("attachment",file.getName());
// 下载文件
return new ResponseEntity<byte[]>(Files.readAllBytes(file.toPath()),headers, HttpStatus.OK);
}
}
-
拦截器
-
拦截器与过滤器的区别在于
域不同,也就是范围不同,过滤器的范围要大的多,将拦截器涵盖在内 -
拦截器的具体实现
-
类去实现 HandlerInterceptor 接口即可,重写方法
preHandle,postHandle,afterCompletion-
@Component
public class InterceptorTest03 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("InterceptorTest03's preHandle execute!");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("InterceptorTest03's postHandle execute!");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("InterceptorTest03's afterCompletion execute!");
}
}
-
-
preHandle:在Controller之前进行拦截,返回True放行,false拦截。
postHandle:在Controller之后执行,不进行拦截操作了。
afterCompletion:在渲染视图结束后执行。
-
将拦截器挂载到 IoC 即可
-
<mvc:interceptors>
<mvc:interceptor>
<!--对多层路径进行拦截-->
<mvc:mapping path="/**"/>
<!--排除 /ok 路径,对 /ok 请求不进行拦截-->
<mvc:exclude-mapping path="/ok"/>
<!--配置拦截器-->
<ref bean="interceptorTest01"/>
</mvc:interceptor>
</mvc:interceptors>
-
-
拦截器的运行流程
- 如果,其中一个拦截器返回 false,则整个的流程如下
- 拦截器集合:[I1,I2,I3.....Ii....In]
- 先说结论:类似于栈的方式,先进后出。
- 如果有一个拦截器返回 false,则后续拦截器的 postHandler 方法不执行,然后开始倒叙执行
afterCompletion方法。、 - 流程:
[preI1,preI2,preI3,preI4......preIi]--->[aC(i-1),aC(i-2),aC(i-3).....aC3,aC2,aC1]