目录
[Spring MVC与Tomcat](#Spring MVC与Tomcat)
拦截器类需要实现HandlerInterceptor,这个接口
前言:
首先先介绍一下Servlet这个用java编写的服务端的程序。再介绍Cookie和Session的会话技术,最后再介绍SpringMVC,因为现在的表述层都直接用SpringMVC这个框架了,淡化了很多Servlet方面的知识,不过这个框架底层也是用Servlet实现的,所以在此简单记录一下。
Servlet:
Servlet是Java编程语言用于在Java EE(Enterprise Edition)环境中扩展服务器功能的一个技术。Servlet主要用于创建动态Web内容,可以处理来自客户端(通常是浏览器)的请求,并生成响应。
我们先来认识一下Tomcat的工作机制:
服务端发送请求到web服务器(Tomcat)
web服务器创建一个Servlet,用以后续处理request请求
Tomcat解析这个请求成request,交给Servlet处理,处理结束返回一个响应头
响应头被Tomcat转化成http响应发回服务器浏览器
从这个流程中我们也能看出来,Servlet是一个处理http请求的接口或者说叫类。
后面介绍SpringMVC中的DispathServlet是这个的一个实现接口。
http请求的内容:
http响应的内容:
会话技术:
1:会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
2:会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
3:会话跟踪方案:
客户端会话跟踪技术:Cookie(浏览器端)
服务端会话跟踪技术:Session(后端)
令牌技术
Cookie:
cookie其实就是一些数据信息,类型为"小型文本文件",存储于电脑上的文本文件中
我们想象一个场景,当我们打开一个网站时,如果这个网站我们曾经登录过,那么当我们再次打开网站时,发现就不需要再次登录了,而是直接进入了首页。例如bilibili,csdn等网站。
这是怎么做到的呢?其实就是游览器保存了我们的cookie,里面记录了一些信息,当然,这些cookie是服务器创建后返回给游览器的。游览器只进行了保存。下面展示bilibili网站保存的cookie。
请求(浏览器对后端):(Cookie:name=value)
响应(后端对浏览器):(Set-Cookie:name=value)
Session:
Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上(内存或硬盘):从这句话可以简单理解Cookie是存储在客户端浏览器上的,而Session是存储在服务器后端上的,数据保存起来更安全
介绍完上面的概念之后,秉承着在Java中万物皆可对象的思想
我就引出在项目中会使用的接口:
HttpServletRequest和HttpServletResponse
HttpServletRequest:是Java Servlet API中的一个接口,它代表客户端向服务器发送的HTTP请求。它提供了许多方法,允许开发者从请求中获取信息,如请求参数、请求头、Cookie、会话信息
常用方法:
获取请求参数:
String getParameter(String name)
:获取指定参数的值(通常用于处理表单提交)。String[] getParameterValues(String name)
:获取指定参数的所有值,适用于多选框或多个相同名称的参数。获取请求头:
String getHeader(String name)
:获取指定名称的请求头值,比如User-Agent
、Accept-Language
等。Enumeration<String> getHeaderNames()
:获取请求中所有请求头的名称。获取请求URI和URL:
String getRequestURI()
:获取请求的URI部分(不包括查询参数)。String getRequestURL()
:获取请求的完整URL,包括协议、服务器名和端口号。获取HTTP方法:
String getMethod()
:获取请求使用的HTTP方法,如GET
、POST
、PUT
等。获取客户端信息:
String getRemoteAddr()
:获取发起请求的客户端的IP地址。String getRemoteHost()
:获取发起请求的客户端的主机名。获取会话信息:
HttpSession getSession()
:获取当前请求对应的会话对象,如果没有则创建一个新的会话。HttpSession getSession(boolean create)
:获取当前请求的会话对象,如果没有且create
参数为true
,则创建一个新的会话。获取Cookie:
Cookie[] getCookies()
:获取请求中包含的所有Cookie,以数组形式返回。获取请求属性:
Object getAttribute(String name)
:获取请求中指定的属性值(通常用于在请求之间传递数据)。void setAttribute(String name, Object o)
:设置请求属性。
下面举一个项目中的案例来解释一下这个的用途
背景很简单,就是一个登录用户中心的案例。
整体的设计思想:
比如当我登录之后,我们可能要访问项目中得其它功能,那这个时候问题就来了,我们调用接口的时候,怎么知道你现在是不是已经登录了呢?
这里其实涉及到属性共享作用域的概念。
在 JavaWeb 中,共享域指的是在 Servlet 中存储数据,以便在同一 Web 应用程序的多个组件中进行共享和访问
比如我们想要跳转到另一个页面,并且将页面传输到下一个页面中去
我们就可以用一个共享的存储空间。
这就用到了HttpServletRequest中的getAttribute,setAttribute和removeAttribute方法
实现思路:我们根据上面的知识得知,浏览器会存一个cookie,后端会存一个session
我们通过在HttpServletRequest设置会话属性。
我们登录之后,我们在用户的session中设置一个登录态,这个登录态也可以理解为登录的一个标志,说白了就是一个字符串。
然后我们每次调用其它接口的时候,需要获取当前用户,我们获取当前用户从用户的session中获取他的登录态,然后我们验证这个登录态,如果是我们的标志,就说明这个是已经登录的用户。
案例实现代码:
@Override
public User userLogin(String userAccount, String password, HttpServletRequest httpServletRequest) {
//1:校验输入的账户,密码和校验码是否非空
//2:从数据库中查询用户
//3:用户脱敏
/**
* 其实这个脱敏就是创建一个新的用户封装一些我们想让前端看到的值,保证用户隐私
*/
getSaftyUser(user);
//4:设置用户登录态
httpServletRequest.getSession().setAttribute(UserConstant.USER_LOGIN_STATE,user);
return user;
}
最后一行代码是这个案例实现的核心。
然后我们在其它的方法中获取这个登录态:
@Override
public User getLoginUser(HttpServletRequest httpServletRequest) {
final Object userObj = httpServletRequest.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
User user = (User) userObj;
return user;
}
只要返回的user不空就说明此时登录的状态是有效的。
退出登录:
@Override
public Integer userLogout(HttpServletRequest request) {
request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE);
return 1;
}
退出登录就很简单了,只需要移除了这个登录态就行。
小总结(感想):
这个是鱼皮老师的用户中心项目,之前写过苍穹外卖,接触了jwt登录,但是那个时候是直接用了拦截器,自己对jwt的研究也不够深入,这个项目接触了另一种登录方法。也算是有所收获。
不过除了这个收获之外,我觉得更多我需要去意识到的是,有请求和响应这个东西,这个东西每个人都知道,但是在项目中就不晓得用。所以在写完这个总结之后,我觉得更多的想法是在以后的项目中,脑袋里要有请求接口HttpServletRequest和响应接口HttpServletResponse这两个东西。
SpringMVC介绍:
Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring Framework中。正式名称"Spring Web MVC"来自其源模块的名称( spring-webmvc
),但它通常被称为"Spring MVC"
为什么springMVC成为现在主流的表述层框架,我认为最主要的就是它可以和springboot无缝衔接,Spring 家族原生产品,与IOC容器等基础设施无缝对接。
调用流程:
来一张清晰一点的图
整体的流程就是用户向后端发送http请求,(以前听说是直接自己写原生Servlet API开发代码进行接受参数(我也没用过),现在变成了是基于springMVC开发代码进行开发)
首先这个请求先发到DispatchServlet上(可以理解这个为CEO)
如果想更好理解这个DispatchServlet,我们需要先了解Servlet和Tomcat容器
Servlet和Tomcat容器:
Tomcat是一个开源的Java Servlet容器,是Java EE(现在称为Jakarta EE)技术的一部分。它可以用来运行基于Java的Web应用程序,包括使用Spring MVC框架开发的应用程序。
Tomcat的作用
- Servlet容器: Tomcat负责管理和执行Java Servlet(后端代码)的生命周期。Servlet是Java用于处理HTTP请求和响应的组件。
- Web服务器: Tomcat也可以作为一个简单的Web服务器,能够处理HTTP请求,提供静态内容(如HTML、CSS、JavaScript文件等)。
Spring MVC与Tomcat
在使用Spring MVC开发Web应用时,通常需要一个Servlet容器来处理请求,并将其转发到相应的Controller。在你提到的项目中,Tomcat负责启动应用程序并处理来自用户的HTTP请求。
最后再说一句题外话,在我们写springboot项目的时候,我们没有去配置什么Tomcat,是因为springboot内嵌了这个web容器。简化了开发过程
说回调用的流程。
在DispatchServlet收到请求之后,CEO首先找的就是一个秘书:handlerMapping,这个秘书的作用就是来匹配路径,我们在Controller层都写过
这个@RequestMapping可以加在类上也可以在方法上,一般都是类上
在类上加了这个方法之后,说明在下面的方法的路径前面都会自动拼上/user。
然后这个秘书handlerMapping找到了匹配的路径返回对应的方法给这个DispatchServlet,
接着DispatchServlet就会去找经理:handlerAdapter
这个经理的作用是什么呢?
这就需要说回SpringMVC的一个重要作用了:
-
简化前端参数接收( 形参列表 )
-
简化后端数据响应(返回值)
这里的handlerAdapter就是做这两件事情的。
最后经过一系列的操作,终于访问到了这个方法上,方法返回值一层一层向上走,返回个DispatchServlet。
SpringMVC接受和响应数据:
访问路径设置:
上面说过了@RequestMapping这个注解加在类上,后面的方法的路径都会拼上这个注解后面的路径。
精准路径匹配:
这个应该就是最常见的
比如这个例子,就是精准的指定了路径 "/user/register"
模糊路径匹配:
这个其实在Controller里面用的不多。
用的地方是WebConfigure
首先说一下规则
单层匹配和多层匹配:
/*:只能匹配URL地址中的一层,如果想准确匹配两层,那么就写"/*/*"以此类推。
比如你写user/*,那么这个地址就可以匹配user/a或者user/aa,但是不能user/a/a。
/**:可以匹配URL地址中的多层。
这里的多层就是可以匹配user/a/aaa/a,都可以匹配
其中所谓的一层或多层是指一个URL地址字符串被"/"划分出来的各个层次
这个知识点虽然对于@RequestMapping注解来说实用性不大,但是将来配置拦截器的时候也遵循这个规则。
当后端配置跨域的时候就可以用这种模糊地址匹配的方式
附带请求方式限制:
首先我们指定http请求方式有很多种,最常见的Get,Post,Put,Delete
用的最多应该是Get和Post。
根据这些方法,@RequestMapping衍生了出了变体:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
我们找一个注解点进去:
背后的原理其实就是利用@RequestMapping的method方法指定了请求方式,其它方式都不能访问
要不然就会报405异常。
接受参数:
param参数:
首先我们需要指定param参数就是在路径名种拼key=value这种形式:
类似于http://localhost:8080/param/value?name=xx&age=18
1:直接接值,或者用实体类直接接值
@Controller
@RequestMapping("param")
public class ParamController {
/**
* 前端请求: http://localhost:8080/param/value?name=xx&age=18
*
* 可以利用形参列表,直接接收前端传递的param参数!
* 要求: 参数名 = 形参名
* 类型相同
* 出现乱码正常,json接收具体解决!!
* @return 返回前端数据
*/
@GetMapping(value="/value")
@ResponseBody
public String setupForm(String name,int age){
System.out.println("name = " + name + ", age = " + age);
return name + age;
}
}
实体类接受值
实体类:
public class User {
private String name;
private int age = 18;
// getter 和 setter 略
}
@Controller
@RequestMapping("param")
public class ParamController {
@RequestMapping(value = "/user", method = RequestMethod.POST)
@ResponseBody
public String addUser(User user) {
// 在这里可以使用 user 对象的属性来接收请求参数
System.out.println("user = " + user);
return "success";
}
}
上面两种方式直接接值,都必须有一个前提:就是属性名必须等于参数名
还有一个注意点:后面会讲到路径参数,路径参数也是直接在路径上面拼属性,
当我们后面会知道路径参数要用**@PathVariable,如果没有用这个注解,那么还是默认用param方式接受参数**
在实际开发中用实体类来接受参数是比较常用的。
2:@RequestParam注解
可以使用 @RequestParam
注释将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。
说明白点就是将我们想要的属性名绑定到我们想要绑定的参数上。
注意点:
指定绑定的请求参数名
要求请求参数必须传递
为请求参数提供默认值
上面直接上个案例:
这是前端请求的地址:
前端请求: http://localhost:8080/param/data?name=xx\&stuAge=18
这是后端的代码
@GetMapping(value="/data")
@ResponseBody
public Object paramForm(@RequestParam("name") String name,
@RequestParam(value = "stuAge",required = false,defaultValue = "18") int age){
System.out.println("name = " + name + ", age = " + age);
return name+age;
}
首先我们可以看到我们的参数名是age,但是前端传过来的是stuAge,这个时候我们就可以通过@RequestParam来绑定属性值。
第二个就是我们可以设置required为false,这样如果没有这个参数也不会报错
第三个就是设置了默认值,如果你没有给我传值,我就使用默认值
路径参数:
路径传递参数是一种在 URL 路径中传递参数的方式。在 RESTful 的 Web 应用程序中,经常使用路径传递参数来表示资源的唯一标识符或更复杂的表示方式。而 Spring MVC 框架提供了 @PathVariable
注解来处理路径传递参数
直接来一段实例代码:
前端发送请求:
<script>
import { ref } from 'vue';
export default {
setup() {
const username = ref('');
const userInfo = ref('');
const fetchUserInfo = async () => {
try {
const response = await fetch(`/api/users/${username.value}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
userInfo.value = await response.text();
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
};
return {
username,
userInfo,
fetchUserInfo
};
},
};
</script>
前端用axios发送请求,并将username的值直接用¥{}直接拼在路径里面
后端controller:
@RestController
public class UserController {
@GetMapping("/api/users/{username}")
public String getUser(@PathVariable String username) {
return "Hello, " + username + "!";
}
}
后端在接受路径参数的时候用了@PathVariable注解。
JSON参数:
前端传递 JSON 数据时,Spring MVC 框架可以使用 @RequestBody
注解来将 JSON 数据转换为 Java 对象。@RequestBody
注解表示当前方法参数的值应该从请求体中获取,并且需要指定 value 属性来指示请求体应该映射到哪个参数上,并且只能用POST的方式请求
前端发送的请求:
{
"name": "张三",
"age": 18,
"gender": "男"
}
后端接受JSON数据的Java类:
public class Person {
private String name;
private int age;
private String gender;
// getter 和 setter 略
}
Controller:
@PostMapping("/person")
@ResponseBody
public String addPerson(@RequestBody Person person) {
// 在这里可以使用 person 对象来操作 JSON 数据中包含的属性
return "success";
}
@ResponseBody
@ResponseBody
注解,用于将方法返回的对象序列化为 JSON 或 XML 格式的数据,并发送给客户端
这个作为一个补充的知识点即可。
我们点进@RestController这个注解
这里封装了两个注解
一个是@Controller,就是告诉IOC容器,这是一个表示层的bean
另一个就是这个@ResponseBody,这个注解的作用就是将这个类中所有方法的返回值序列化成JSON格式的数据返回给前端。
SpringMVC拦截器:
在讲拦截器之前,先说一下这个Javaweb的一个过滤器Filter
Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
对比一下拦截器:
我们上面说过,http请求是被DispatcherServlet对象先接受,然后DispatcherServlet通过一系列过程调用控制层方法,拦截器就是在调用控制层方法之前进行拦截,而过滤器是在http请求在DispatcherServlet接受到之前就拦截了。
对比了拦截器和过滤器,下面再来说说拦截器
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
常见的项目使用场景就是登录校验:在你访问这个系统其它功能的时候对你进行是否登录的校验
下面来一个登录的案例来讲解拦截器的使用步骤:
下面是一个关于jwt登录校验的案例,下面先记录步骤,等过几天这个SSM课程完结的时候需要写一个小系统,会用到jwt,那个时候再记录jwt的具体步骤和概念
一:定义拦截器类(声明拦截器):
@Slf4j
@Component
public class LoginCheckinterceptor implements HandlerInterceptor {
@Override//在目标资源方法运行前运行,true:放行 false:不放行
//这里的目标资源方法可以理解为controller中的方法
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1:获取请求的url
String url = request.getRequestURI().toString();
log.info("请求的url:{}",url);
//2:判断请求的url中是否包含login,如果包含,说明是登录操作,直接放行
if(url.contains("login")){
log.info("这个是登录操作");
return true;
}
//3:获取请求头中的令牌(token)
String jwt = request.getHeader("token");
//4:判断令牌是否存在,如果不存在,返回错误结果
if(!StringUtils.hasLength(jwt)){
log.info("请求头响应为空,返回错误结果");
Result error = Result.error("NOT_LOGIN");
//手动返回json对象
String notLogin = JSONObject.toJSONString(error);
//封装成json对象之后,由res返回给浏览器
return false;
}
//5:解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("令牌存在异常,返回错误结果");
Result error = Result.error("NOT_LOGIN");
//手动返回json对象
String notLogin = JSONObject.toJSONString(error);
//封装成json对象之后,由res返回给浏览器
return false;
}
//6:放行
log.info("令牌合理,放行");
return true;
}
}
拦截器类需要实现HandlerInterceptor,这个接口
我们具体点进来看一下
这里面有三个方法我们可以去重写,为什么是可以呢,具体看那个default,JDK8之后加入了这一个点,就可以选择方法重写了
首先是preHandle方法,这个是用得最多的,用的地点就是在访问controller方法之前
接着是postHandle方法,这个方法指调用完方法之后,常用来检测是否有敏感词出现。
最后是那个afterCompletion方法,这个是DispatcherServlet返回给浏览器时候的方法了,不经常用
我们再来看一下这这个拦截器里面的逻辑:
- 先从请求request中拿出url
- 根据url判断这个请求是否是登录请求,就是是否包含login,是,则直接放行
- 接着从请求头中取出jwt令牌
- 解析jwt令牌
- 根据解析结果是否成功决定是否放行
二:修改SpringMVC配置类添加拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckinterceptor loginCheckinterceptor;
public void addInterceptor(InterceptorRegistry interceptorRegistry){
interceptorRegistry.addInterceptor(loginCheckinterceptor).addPathPatterns("/**").excludePathPatterns("/*");
}
}
WebMvcConfigurer
是 Spring Framework 中的一个接口,属于 Spring MVC 模块。它允许开发者定制和扩展 Spring MVC 的配置
具体看里面的逻辑:
interceptorRegistry.addInterceptor(loginCheckinterceptor).addPathPatterns("/**");
addPathPatterns("/**"),上面说过这个路径的匹配规则,如果是/**,就说明是所有路径,