mvc理论
项目实战注意事项及idea实战的使用方法
1.链接数据库
点击这几个按键找到自己使用的数据库就可以找到数据库,然后显示你想要使用的数据库,可以将表罗列在右边以便于开发
2.注意重写一些方法
我们在项目中定义好了一个类需要重写好equals还有hashcode方法,以及成员变量的对应get和set方法,
但是因为如果在类内重写好这些方法,但是因为一些原因需要更改成员变量,那么就需要一个方法一个方法的去修改,非常耗时间,所以我们选择使用一个idea的插件,名字叫lombok通过注解的方式就可以实现这些方法的自动实现和实时修改,需要注意的的是,插件启用前我们需要进行一些设置,以图例展示要进行什么设置,这样就可以通过三个注解的方式来实现这些方法的动态改变
package com.my.schedule.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Objects;
/*
* 1.表名和类名应该一致(如果遇见数据库表格中的命名有下划线需要进行驼峰转换)
* 2.类名和列名保持一致(同上)
*3,必须具备get和set
*4.必须具备无参构造器
* 5.应该实现序列化接口(缓存 分布式项目管理 可能会将对象序列化)
* 6.应该重写类的hashcode和equals方法
* 7.tostring是否重写都可以
*
* 8.使用idea自带的get 和set还有午餐构造还有equal重写有缺点
* 假如成员变量进行了修改,那么代码也要跟着修改,所以为了避免这些麻烦
* 我们使用lombok插件
* 1.检查idea是否安装lombok插件
* 2.检查是否勾选了enable annotation processer
* 3.在实体类上添加注解,记得需要导入依赖包
*
* */
@AllArgsConstructor //添加了无参构造
@NoArgsConstructor //添加全参构造
@Data //添加了getter setter equals hashcode
public class SysUser implements Serializable {
private Integer uid;
private String username;
private String userPwd;
}
接下来的回答是解决了我的一些疑惑,复习了java基础
在Java中,当你定义了一个类并且希望这个类的对象能够作为HashMap的键(key),或者放入HashSet、HashTable等基于哈希表的数据结构中时,重写
hashCode()
和equals()
方法变得尤为重要。这样做的目的主要是为了保证这些基于哈希表的数据结构能够正确地存储、查找和删除对象,避免出现意外的行为,比如无法正确检索到已存储的对象或错误地存储了重复的对象。为什么需要重写
hashCode()
和equals()
方法?
保证唯一性:在HashSet或HashTable中,每个元素(或键值对中的键)必须是唯一的。通过重写
equals()
方法,我们可以定义两个对象何时被认为是相等的。如果没有重写equals()
方法,那么就会使用Object类的equals()
方法,它仅仅比较对象的引用(即地址),这通常不是我们想要的行为。优化查找效率:HashMap、HashSet等集合使用哈希表来存储元素,哈希表通过计算元素的哈希码(通过
hashCode()
方法)来确定元素在表中的位置(桶位置)。如果两个对象通过equals()
方法比较是相等的,那么它们的哈希码也必须相同,这是哈希表正确工作的前提条件。如果重写了equals()
但没有重写hashCode()
,或者反之,就可能导致哈希表无法正确地将对象定位到同一个桶中,从而影响查找效率,甚至导致无法正确存储或检索对象。注意事项
- 当重写
equals()
方法时,通常也需要重写hashCode()
方法,以确保满足equals()
相等的对象具有相同的哈希码。hashCode()
方法必须是一致的,即对于给定的对象,在Java应用执行期间,只要对象的equals比较中所用的信息没有被修改,那么对这个对象多次调用hashCode方法必须始终如一地返回同一个整数。- 不同的对象可以有相同的哈希码(哈希冲突),但相等的对象必须有相同的哈希码。
示例
假设我们有一个
Person
类,并且我们想要将Person
对象作为键存储在HashMap中:
java复制代码
public class Person {
private String name;
private int age;
// 构造函数、getter和setter省略
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 使用Objects类的hash方法,它利用了Objects的hash算法来生成哈希码
}
}
在这个例子中,我们重写了
equals()
和hashCode()
方法,以确保当两个Person
对象的name
和age
都相等时,它们被认为是相等的,并且具有相同的哈希码。这样,我们就可以将Person
对象作为HashMap的键来使用了。public class Person { // 类定义开始,表示这是一个Person类 private String name; // 定义了一个私有成员变量name,用于存储人的名字 private int age; // 定义了一个私有成员变量age,用于存储人的年龄 // 构造函数、getter和setter省略 // 这里省略了构造函数、getter和setter方法,这些方法通常用于初始化对象属性、获取和设置对象的属性值 @Override // 使用@Override注解表明这个方法重写了父类(这里是Object类)中的方法 public boolean equals(Object obj) { // 重写equals方法,用于比较两个对象是否相等 if (this == obj) return true; // 如果当前对象(this)和传入的对象(obj)是同一个对象(即内存地址相同),则直接返回true if (obj == null || getClass() != obj.getClass()) return false; // 如果传入的对象为null,或者传入对象的类型与当前对象不同(即不是同一个类的实例),则直接返回false Person person = (Person) obj; // 将传入的对象强制类型转换为Person类型,因为我们已经通过前面的检查确保了它是Person类型的实例 return age == person.age && Objects.equals(name, person.name); // 比较当前对象和传入对象的age是否相等,以及使用Objects.equals方法比较它们的name是否相等 // 如果两者都相等,则返回true,表示两个对象相等;否则返回false } @Override // 同样使用@Override注解表明这个方法重写了父类中的方法 public int hashCode() { // 重写hashCode方法,用于生成对象的哈希码 return Objects.hash(name, age); // 使用Objects类的hash方法,传入name和age作为参数 // Objects.hash方法会根据传入的参数生成一个哈希码,这里利用了name和age的值来生成Person对象的哈希码 // 这样做可以确保当两个Person对象的name和age都相等时,它们的哈希码也相等 } }
3.在idea中使用数据库查询语句来看看返回的是一个什么类型的值
4.项目实战
首先是认识项目的目录结构然后注意讲解各个包里面的类是什么作用
1.controller介绍
这一部分类对应的MVC结构中的控制层,获取用户想要进行的操作并转交给后端
接下来以代码进行具体编码讲解
1.BaseController
package com.my.controller;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class BaseController extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//首先获取请求路径
String requestURI = req.getRequestURI();
//然后通过符号进行切割以获得最后的一个uri
String[] strings = requestURI.split("/");
String methodName = strings[strings.length - 1];
//通过反射获取到本类的对象,以便于调用对应的方法
Class aclass=this.getClass();//
//获取到方法
try {
Method method = aclass.getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
//通过暴力破解方法的权限执行方法
method.setAccessible(true);
//执行方法
//这里this关键字指的是当前对象实例,即BaseController的一个实例。
// 由于你正在调用BaseController类中的一个实例方法(通过反射),
// 你需要传递一个BaseController的实例来调用该方法。在这个上下文中,
// this就是当前BaseController实例的引用。
method.invoke(this,req,resp);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
这一串代码是一个高复用性代码
首先我们通过req对象获取了客户端发送过来的请求URI来进行判断客户端想要进行什么样的业务
通过getRequestURI()获取到了发送过来的uri但是这个uri是一个一整串的通过“/”进行分割的uri,我们只需要知道他最后一个分割符后的业务是什么就行了,使用String中的split()方法将其分割成一个一个的字符串,用一个字符串数组接收,然后找到最后一个字符获取到客户端的需求是什么,
然后我们通过反射Class aclass=this.getClass();获取到了本类的实例,接着,通过本类的实例对象调用getDeclaredMethod()方法通过明确的函数名字和参数名字来获取到对应的方法,
Method method = aclass.getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
解释一下这个方法的各个参数传入进来的目的,首先是methodName这个参数是告知方法名是什么,然后剩下的就是通过反射将参数对应的类传入getDeclaredMethod()告知这个方法的参数是什么,以便于获取到正确的方法,然后我们再获取到方法的对象的时候再通过
method.invoke(this,req,resp);
方法执行函数,这个方法的参数分别的功能,是this的传入代表着执行这个方法的对象,req和resp的传入代表着参数的实例对象被传入,然后执行这个方法
通过以上代码就可以实现通过一段高度复用的代码实现不同的业务统一处理
接下来通过继承了BaseController的类来进行讲解怎么做到不同业务不同处理
package com.my.controller;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 增加日程的路径 /schedule/add
* 删除日程的路径 /schedule/remove
* 修改日程的路径 /schedule/update
* 查找日程的路径 /schedule/find
*/
@WebServlet("/SysSchedule/*")//通过添加/*来设置一个占位符,使得浏览器得知后面应该有一串字符
public class SysScheduleController extends BaseController {
protected void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("add");
}
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("update");
}
protected void remove(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("remove");
}
protected void find(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("find");
}
}
首先是我们在@WebServlet("/SysSchedule/*")这里后面注意加上一个/*,这样就可以告知浏览器,在localhost:8080/SysServlet这个uri后面还可以跟着一个路径来代表着不同的业务,就像在注释中写的哪样,然后我们将方法名称写的和最后一段路径一样,这样就能够通过BaseController中的那一串代码获取到最后一个路径的名称然后通过这个名称执行对应的方法执行代码,通过这种方式,我们就不需要在各个Servlet里面进行service方法的重写,只需要写好不同业务对应的方法该如何处理业务就可以了,大大提高了效率
2.DAO包介绍
这个包里面存放的方法的目的是对数据库表格进行增删改查的操作,
我们通过JDBC的学习写下BaseDao来进行数据的写入,我拿SysScheduleDao举例,我们想对Schedule表进行一个数据操作我们先定义一个接口,在这个接口里面明确应该要有什么样的业务,明确好了业务的规范再编写实现类对业务进行具体的实现
接下来展示代码
SysScheduleDao接口:
package com.my.Dao;
import com.my.schedule.pojo.SysSchedule;
import java.util.List;
public interface SysScheduleDao {
int addSchedule(SysSchedule schedule);
/**
* 查询所有用户的所有日程
* @return 将所有的日程放入一个list容器里面
*/
List<SysSchedule> findAllSchedule();
}
首先我们接口中明确我们需要什么方法来完成对数据库的连接,写好注释以便于你的同事使用
SysScheduleDao实现类:
package com.my.Dao.impl;
import com.my.Dao.BaseDao;
import com.my.Dao.SysScheduleDao;
import com.my.schedule.pojo.SysSchedule;
import java.util.List;
//通过继承basedao来直接调用里面的方法插入数据
public class SysScheduleImp extends BaseDao implements SysScheduleDao {
@Override
public int addSchedule(SysSchedule schedule) {
String sql="insert into sys_schedule values(DEFAULT,?,?,?)";
//这里的get方法是通过注解实现的
int baseUpdate = baseUpdate(sql, schedule.getUid(), schedule.getTitle(), schedule.getCompleted());
return baseUpdate;
}
@Override
public List<SysSchedule> findAllSchedule() {
String sql="select sid,uid,title,completed from sys_schedule";
List<SysSchedule> list = baseQuery(SysSchedule.class, sql);
return list;
}
}
我们在里面通过sql语句对表进行操作并返回影响的行数,当涉及到多行多列的对象时我们需要使用一个能够和数据库的列名匹配上的类来接收他的数据,并将它变成列表储存
3.表的实体类介绍
我们可以发现在数据库中有两个表,其中每个表有多个列,这时我们就需要创建一个表的实体类,对数据库中的数据进行接收,以类进行分类方便程序的编写和数据的获取
4.Service包介绍
5.test包
一般用于测试一些代码能不能够正常运行,以便于程序的编写和减少bug
package com.my.test;
import com.my.Dao.BaseDao;
import com.my.schedule.pojo.SysUser;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.List;
public class TestBaseDao {
private static BaseDao baseDao;
@BeforeClass //在类初始化时运行这个方法,对baseDao进行初始化
public static void initBaseDao() {
baseDao=new BaseDao();
}
@Test
public void TestBaseDaoQueryObject()
{
//查询用户数量
String sql="select count(1) from sys_user";
//返回查询结果,因为这里面返回的是一个单行单列的数据,count()函数是对应的java中的long类型数据,
// 所以使用long类型接收
Long count = baseDao.baseQueryObject(Long.class, sql);
System.out.println(count);
}
@Test
public void testBaseQuery()
{
String sql="select uid,username,user_pwd userPwd from sys_user";
//这里的pwd为什么要起一个别名?
//因为在SysUser类里面对应的user_pwd的名字时userPwd当我们从数据库查询返回结果时,
// 需要用SysUser对象来进行接收,所以对应的列名需要找到对应的属性名称才能将结果返回并对应,所以就使用起别名的方式返回结果才能正确接收
List<Object> list = baseDao.baseQuery(SysUser.class, sql);
list.forEach(System.out::println);
}
@Test
public void testBaseUpdate()
{
String sql="insert into sys_schedule values(DEFAULT,?,?,?)";
int java = baseDao.baseUpdate(sql, 1, "学习java", 1);
System.out.println(java);
}
}
6.Util工具类
一般用于存放一些工具,比如JDBC工具,比如加密工具
7.会话层和监听器实现登录业务
基本思路,通过登录时创建一个session然后将SysUser放入session中,然后再通过filter判断有没有session中存放的Sysuser对象,如果有就说明能够通过,没有就说明不能
以下是代码实现
package com.my.Filter;
import com.my.schedule.pojo.SysUser;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebFilter(urlPatterns = {"/ShowSchedule.html","/SysSchedule/*"})
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//参数父转子
HttpServletResponse resp = (HttpServletResponse) servletResponse;
HttpServletRequest req = (HttpServletRequest) servletRequest;
//获得session域对象
HttpSession session = req.getSession();
SysUser sysUser =(SysUser) session.getAttribute("SysUser");
if (sysUser == null) {
resp.sendRedirect( "/login.html");
}
else
{ //放行
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
由上述代码看到这个过滤器针对
"/ShowSchedule.html","/SysSchedule/*"
这两个比较隐私的请求做出限制,会自动跳转到登陆页面
8.Ajax实战展示
我们想要在这个页面上实现实时判断用户名有没有被占用,仅凭前端就不能实现了,所以接下来我们使用Ajax技术实现
接下来展示关键代码
首先是js:
function checkUsername(){
var usernameReg = /^[a-zA-Z0-9]{5,10}$/
var usernameInput = document.getElementById("usernameInput")
var username = usernameInput.value
var usernameMsg = document.getElementById("usernameMsg")
if(!usernameReg.test(username)){
usernameMsg.innerText="格式有误"
return false
}
//格式正确之后我们来判断用户名是否被占用
var request = new XMLHttpRequest();
//书写回调函数,当请求被响应后且成功了就调用
request.onreadystatechange=function ()
{
if(request.readyState==4&&request.status==200)
{ //把后端响应回来的文本显示在频幕上
usernameMsg.innerText=request.responseText;
}
}
//接下来设置请求方式和路径,向谁发送请求,发送请求干什么,带什么参数,注意此时还没有发送,只是完成了发送前的设置
request.open("GET","/user/checkUserNameUsed?username="+username);
//发送请求
request.send();
// usernameMsg.innerText="OK"
// return true
}
在上述代码中,我们new XMLHttpRequest()对象,向后端发送了从前端获取到的用户名,并请求Servlet对象进行处理,接下来看Java代码如何处理
java:
protected void checkUserNameUsed(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收用户名
String username = req.getParameter("username");
//查询用户名是否被使用,如果没有这个username那么返回的对象就是一个空对象
SysUser sysUser = userService.findByUsername(username);
String info="可用";
//如果有啧响应已经占用
if(sysUser!=null){
info="不可用";
}
resp.getWriter().print(info);
}
在这个代码中,我们可以看到通过req对象获取到前端发送的数据信息,然后发送给service层联系DAO层返回一个从数据库中获取到的SysUser对象判断是否为空就可以判断用户名是否被占用
接下来是图片展示
我们可以看到当用户取消输入时,调用了回调函数向后端发送了请求,并传递了数据
响应json问题分析
/** * 1.响应乱码问题 * 2.响应格式不规范,处理方式不规范 * 后端相应回来的信息应该有一个统一的格式,前后端共享 * 响应一个JSON串(前后端约定好的一个相应格式,方便代码的编写和修改) * { * "code":"",业务状态码(自定义的数字,通过不同的数字判断业务的状态是什么) 本次请求的业务是否成功?如果失败了,是什么原因失败了 * "message","业务状态码的补充说明,即说明这个状态码下业务是发生了什么情况,因为单纯只有状态码的话有可能会忘记业务是干什么事的" * "data",{} 本次相应的数据 成功/不成功 list<Schedule> .... * } * 3.校验不通过,无法阻止表单提交的问题 */
简单来说就是如果没有一个规范的格式,那么前后端修改会非常的麻烦
那么我们规范了一个json串的格式,如果在后端的代码自己手动写字符串格式返回给前端会非常麻烦,所以我们采用一种自己编写的工具类将json串以对象的形式传参存储会方便很多
首先我们创建两个类,一个叫Result一个叫ResultCodeEnum
首先我们展示枚举类ResultCodeEnum
这里面包含了我们需要的各种情况的字符串常量的对应的事件代码和事件描述,方便以后添加维护
在这里面我们定义了五个枚举量,这五个枚举量会自动调用构造方法并传参
package com.my.common;
public enum ResultCodeEnum {
//这是一个枚举类型的量,如果这样书写的话那么就会通过SUCESS调用构造器
SUCCESS(200,"sucess"),
USERNAME_ERROR(501,"userNameError"),
PASSWORD_ERROR (300,"password_error"),
NOTLOGIN(400,"notLogin"),
USERNAME_USED(600,"username_used");
private Integer code;
private String message;
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
//不让别人使用
private ResultCodeEnum(Integer code, String msg) {
this.code = code;
this.message = msg;
}
}
接下来展示Result类:
在这里面我们定义了几个私有变量,这几个私有变量是我们事先商量好的JSON串的格式,其中data采用泛型适应各种类型的变量,代码的含义在注释中有写道
package com.my.common;
/**
* 全局统一响应的JSON格式处理类
*
*/
public class Result<T> {
// 返回码
private Integer code;
// 返回消息
private String message;
// 返回数据
private T data;
public Result(){}
// 返回数据
//这个方法规定了传进来的数据是什么,将数据传给data对象,规定数据类型是什么,然后在下面的build中调用
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null)
result.setData(data);
return result;
}
//这个方法规定了数据的值和数据的状态码和响应信息
public static <T> Result<T> build(T body, Integer code, String message) {
Result<T> result = build(body);
result.setCode(code);
result.setMessage(message);
return result;
}
//通过ok方法返回一个在ok中传进来的result变量,还有ok中生成的枚举量
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
/**
* 操作成功
* @param data baseCategory1List
* @param <T>
* @return
*/
public static<T> Result<T> ok(T data){
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}
public Result<T> message(String msg){
this.setMessage(msg);
return this;
}
public Result<T> code(Integer code){
this.setCode(code);
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
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;
}
}
接着我们在前端部分采用了这样的代码
通过checkUsername()这个方法中我们通过var request = new XMLHttpRequest();获取到了请求报文的对象,然后定义了回调函数,接着我们使用了下面这两个方法规定了请求发给谁
//接下来设置请求方式和路径,向谁发送请求,发送请求干什么,带什么参数,注意此时还没有发送,只是完成了发送前的设置 request.open("GET","/user/checkUserNameUsed?username="+username); //发送请求 request.send();
发送给后端后调用回调函数,通过后端传回来的readyState和status的代码判断该执行什么样的逻辑
<script>
function checkUsername(){
var usernameReg = /^[a-zA-Z0-9]{5,10}$/
var usernameInput = document.getElementById("usernameInput")
var username = usernameInput.value
var usernameMsg = document.getElementById("usernameMsg")
if(!usernameReg.test(username)){
usernameMsg.innerText="格式有误"
return false
}
//格式正确之后我们来判断用户名是否被占用
var request = new XMLHttpRequest();
//书写回调函数,当请求被响应后且成功了就调用
request.onreadystatechange=function ()
{
if(request.readyState==4&&request.status==200)
{ //把后端响应回来的文本显示在频幕上
var result=JSON.parse(request.responseText);
if(result.code==200)
{
usernameMsg.innerText="可用"
}else if (result.code!=200)
{
usernameMsg.innerText="不可用";
}
}
}
//接下来设置请求方式和路径,向谁发送请求,发送请求干什么,带什么参数,注意此时还没有发送,只是完成了发送前的设置
request.open("GET","/user/checkUserNameUsed?username="+username);
//发送请求
request.send();
接着我们展示后端的代码
在后端里我们导入了jar包,这个jar包能够将返回的result对象自动拆解为一个字符串,然后通过响应报文的书写传数据给前端,具体的逻辑注释有写
protected void checkUserNameUsed(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收用户名
String username = req.getParameter("username");
//查询用户名是否被使用,如果没有这个username那么返回的对象就是一个空对象
SysUser sysUser = userService.findByUsername(username);
Result result=Result.ok(null);
//如果有啧响应已经占用
if(sysUser!=null){
result=Result.build(null, ResultCodeEnum.USERNAME_USED);//将一个枚举量中的用户已被注册的信息传送给result
}
//将result对象转换为JSON串相应给客户端
//ObjectMapper 是导入的jar包里面的方法
ObjectMapper objectMapper = new ObjectMapper();
//将这样的result里面包含了json串需要的变量传入objectmapper对象就可以将其转为json串格式返回前端
String info=objectMapper.writeValueAsString(result);
//告诉客户端相应给你的是一个JSON串
resp.setContentType("application/json,charset=UTF-8");
resp.getWriter().print(info);
}
接着我们发现,最后那一段将返回的result对象变成json串的方法以后会经常用到,会将不同的result对象编程json串,所以我们将这个方法封装成一个工具类使用
5.会话层管理
为什么需要会话层管理?
因为HTTP是无状态协议,无状态就是不保存状态,HTTP协议自身不对请求和响应之间的通信状态进行保存,也就是说HTTP协议对发送过的请求或者响应都不做持久化处理
简单理解就是:浏览器发送请求,服务器响应并接收,但是服务器不记录请求是否来自哪个浏览器,服务器没有记录浏览器的特征,就是客户端的状态
那么什么是无状态协议和有状态协议
举个例子:张三去饭店吃饭和老板说点老样子
无状态协议:老板没有记录张三每次吃饭时点的什么菜,所以每次都要询问张三
有状态协议:老板有记录张三每次吃饭点的什么菜,翻看记录查找到了张三之前的菜单,直接下单
那么我们该如何记录客户端的状态呢
会话管理的手段:Cookie和session配合解决
Cookie:是在客户端保留少量数据的技术,主要通过响应头向客户端响应一些客户端要保留的信息
session:是在客户端保留更多数据的技术,主要通过HttpSession对象保存一些和客户端相关的信息
以图例解释
当客户端第一次就当前客户端想要进行的业务向服务端发送请求时,服务端会查找客户端是否携带Cookie,当服务端发现,他没有携带Cookie的时候就会在服务端创建一个Session对象,里面存放着唯一标识这个业务的id,然后再通过响应报文发送Cookie给客户端让客户端记录标识id和携带一些业务的数据,当下一次又进行这次业务的时候,服务端就能通过cookie的id并直接找到相对应的业务逻辑和业务数据,提高了进程效率,在服务端当中每一个session对象都对应了某个客户端
Cookie概述
其运作模式如下图所示
代码展示如下:
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//创建cookie对象
Cookie cookie1=new Cookie("keya","valuea");
Cookie cookie2=new Cookie("keyb","valueb");
//将cookie放入respon对象中
resp.addCookie(cookie1);
resp.addCookie(cookie2);
}
}
当我们请求了这个服务的时候,此时我们可以查看到响应头中存放的信息有我们写上去的两个cookie
当我们请求一个不存在的服务时,我们会发现浏览器的请求标头中保存着这个cookie的键值对,也就是服务端会保存cookie的部分数据信息
Cookie的存续时间
以下是cookie存续相关的api:
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/servletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//创建cookie对象
Cookie cookie1=new Cookie("keya","valuea");
//设置cookie在客户端存续的时间,以秒为单位,不会因为浏览器关闭导致cookie丢失
cookie1.setMaxAge(60*5);
Cookie cookie2=new Cookie("keyb","valueb");
//意思是servletB请求的时候提交这个cookie其他时候不提交
cookie2.setPath("/servletB");
//将cookie放入respon对象中
resp.addCookie(cookie1);
resp.addCookie(cookie2);
}
}
HttpSession概述
每一个客户端请求时都会产生一个session对象并生成对应的JESSIONID
通过在web.xml文件里面可以书写如下代码规定session的存续时间,默认时间是30分钟
<session-config>
<session-timeout>30</session-timeout>
</session-config>
以下是session有关的代码
Servlet1:
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
public class Servlet1 extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取参数中的username
String username = req.getParameter("username");
//获得session对象
HttpSession session = req.getSession();
//判断请求中有没有一个特殊的cookie JSESSIONID 值*** ***
//1.有
// >根据JSESSIONID找到对应的session对象
// 1.找到了
// 返回之前的session
// 2.没找到 (session也是有存续时间的)
// 创建一个新的cookie返回,并且向response对象中存放一个session对象
//2.没有
// >创建一个新的session返回,并且向response对象中存放一个JSESSION
//有这样的api能够设置session的存续时间
session.setMaxInactiveInterval(120);//以秒为单位
System.out.println(session.getId());
// 判断session是不是新生成的
System.out.println(session.isNew());
// 将username存入session
session.setAttribute("username", username);
// 客户端响应信息
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("成功");
}
}
我们通过Servlet1在服务端生成了一个session对象,并向里面注入了数据,接下来我们通过Servlet2获取其中存放的数据
Servlet2:
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
public class Servlet2 extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获得session对象,只要是一个客户端请求的,不管多少次都会拿到这个已经生成的session对象
HttpSession session = req.getSession();
System.out.println(session.getId());
System.out.println(session.isNew());
// 读取session中存储的用户名
String username=(String)session.getAttribute("username");
System.out.println("servlet2 got username"+username);
}
}
首先通过getSession()获取到session对象,为什么能够获取到呢,因为在客户端存在一个id,服务端获取到了这个id就会将相应的session对象赋予给servlet2,然后从中获取到数据
三大域对象
分别是请求域,会话域,应用域
请求域:
以图例解释:
请求域其作用范围是在一次请求之内,即客户端的一次请求可以往里面放入数据和拿出数据,但是不能跨请求拿取数据和放入数据,由图中来看,第一次请求可以放入和拿出数据,但是剩下的两个请求是不能在Servlet2拿到数据的
会话域:
我们在一个会话之内是能够多次从里面拿取数据和放入数据的,也就是说,是同一个session的话是能够跨请求拿取数据的,第一次请求时Servlet2将数据放入了会话域,第二次请求的时候将数据拿出来,是可以实现的,但是不能跨越客户端拿取数据,每个客户端里面都有一个cookie那么就只能通过cookie向会话域拿取数据,其他客户端则拿取不到
应用域:
即只要是多个客户端向同一个应用进行请求的话,是能够共享其中的数据,获取到里面的数据
域对象api
只要是域对象就一定有下面的几个api
1. void setAttribute(String name,String value)向域对象添加数据和修改数据
2.Object getAtribute(String name)从域对象中获取数据
3.remove(String name)从域对象中移除数据
我们通过几个代码来验证几个域的数据传递
ServletA(发送数据给各个域对象):
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/ServletA")
public class ServletA extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//向请求域存放数据
req.setAttribute("request","requestMessage");
//向会话域存放数据
HttpSession session = req.getSession();
session.setAttribute("session","sessionMessage");
//向应用域存放数据
ServletContext application = getServletContext();
application.setAttribute("application",application);
//获取请求域数据,在一次请求之内获取数据
String request = (String)req.getAttribute("request");
System.out.println("请求域"+request);
//请求转发
req.getRequestDispatcher("ServletB").forward(req,resp);
}
}
ServletB(观察是否能够接受到ServletA发送来的数据):
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/ServletB")
public class ServletB extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求域数据
String request = (String)req.getAttribute("request");
System.out.println("请求域"+request);
//获取会话域中的数据
HttpSession session = req.getSession();
String sessionMes = (String) session.getAttribute("session");
System.out.println("会话域"+sessionMes);
//获取请求域中数据
ServletContext application = getServletContext();
System.out.println("请求域"+application.getAttribute("application").toString());
}
}
运行之后我们可以得到以下结果:我们可以发现requestMessage出现了两次输出,即一次是在ServletA中获取到的,这里就体现了一次请求之内可以获取和传递数据,接着下一次出现是ServletB获得的,这里就体现了可以通过请求转发获取数据,
而当我们主动请求ServletB我们就可以发现,假如没有请求转发那么我们就获取不到这个数据了
原因就是他已经是另一个req对象了,每一次请求和重定向都会产生新的req对象,那么数据就是不会传递
接下来我们再看一组请求
这个请求使用了两个浏览器主动请求了ServletB我们可以发现第二个浏览器没有获取到会话域对象,其中,原因是一个会话域只会产生在一个客户端对服务端的请求,当使用另一个客户端时会话域就已经换人了,而应用域是从整个应用的范围内获取数据,此时我们可以发现,这个应用域数据是能够得到保存的,这就验证了我们之前所书写的数据获取的范围