# JavaWeb2

# 1.Thymeleaf渲染流程

1)前端发起请求

2)servlet处理请求,准备渲染数据

// 1. 查询数据(比如水果列表、分页信息)
List<Fruit> fruitList = fruitDao.getFruitList(keyword,pageNo);
int pageCount = (fruitCount + 4) / 5;

// 2. 将数据存入作用域(Thymeleaf只能读取Request/Session/Application作用域的数据)
req.setAttribute("fruitList", fruitList); // Request作用域(推荐)
req.getSession().setAttribute("pageCount", pageCount); // Session作用域
1
2
3
4
5
6
7

3)调用Thymeleaf模板处理器

// ViewBaseServlet的核心逻辑(简化版)
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) {
    // 1. 获取Thymeleaf的核心引擎对象(TemplateEngine)
    TemplateEngine templateEngine = getTemplateEngine(req.getServletContext());

    // 2. 设置模板上下文(封装Request/Session数据)
    WebContext context = new WebContext(req, resp, req.getServletContext());

    // 3. 指定模板文件(比如"index"对应/templates/index.html)
    String templatePath = "/" + templateName + ".html";

    // 4. 核心:渲染模板 → 生成HTML字符串
    String html = templateEngine.process(templatePath, context);

    // 5. 将HTML写入响应,返回给浏览器
    resp.getWriter().write(html);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

4)Thymeleaf 解析模板文件(语法处理)

Thymeleaf 会逐行解析模板文件中的特殊语法,替换为真实数据

5)生成最终 HTML 字符串(数据填充)

6)响应数据浏览器渲染


# 2.DispatchServlet 的三个演变阶段

1)原始阶段 —— 每个操作一个 Servlet(最早期)

// 新增水果的Servlet
@WebServlet("/addFruit.do")
public class AddFruitServlet extends ViewBaseServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        // 仅处理添加水果逻辑
    }
}

// 编辑水果的Servlet
@WebServlet("/editFruit.do")
public class EditFruitServlet extends ViewBaseServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // 仅处理编辑水果逻辑
    }
}

// 删除水果的Servlet
@WebServlet("/delFruit.do")
public class DelFruitServlet extends ViewBaseServlet {
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

问题:Servlet 数量爆炸,每个 Servlet 只处理一个小操作,代码分散,维护困难。

2)基础调度 —— 单 Servlet 多操作

为了解决 Servlet 泛滥问题,演变出 一个业务模块一个 Servlet,通过operate参数区分操作,这是 DispatchServlet 的雏形:

@WebServlet("/fruit.do")
public class FruitServlet extends ViewBaseServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) {
        // 1. 统一处理编码(通用逻辑)
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");

        // 2. 获取操作类型,分发请求
        String operate = req.getParameter("operate");
        switch (operate) {
            case "index": index(req, resp); break; // 列表
            case "add": add(req, resp); break;     // 新增
            case "edit": edit(req, resp); break;   // 编辑
            // 其他操作...
        }
    }

    // 各操作的具体逻辑
    private void index(...) { ... }
    private void add(...) { ... }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

进步点

  • 一个业务模块一个 Servlet,减少 Servlet 数量;
  • 统一处理通用逻辑(编码、异常、日志),避免重复代码;
  • 通过operate参数分发请求,职责更清晰。

仍存在的问题

  • 每个业务模块仍要写一个 Servlet(比如UserServletOrderServlet),还是有冗余;
  • 操作逻辑(index/add/edit)和 Servlet 耦合,不利于扩展;
  • URL 规则(/fruit.do)需要手动维护,容易出错。

也可通过反射优化

 Method[] declaredMethods = this.getClass().getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(operate.equals(method.getName())){
                try {
                    //找到和operate同名的方法,反射调用
                    method.invoke(this, req, resp);
                    return;
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        throw new RuntimeException("operate值非法");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3)成熟 DispatchServlet—— 全局统一调度(Spring MVC 的 DispatchServlet)

核心是一个 DispatchServlet 处理所有请求,通过「URL→控制器方法」的映射关系分发请求,也是 Spring MVC 的核心

流程

浏览器发送请求——》进入DispatchServlet——》解析url,映射handleMapping——》controller、service、mapper——》viewResolve解析模板路径——》view渲染——》返回html

@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet{

    private Map<String,Object> beanMap = new HashMap<>();

    public DispatcherServlet(){
    }

    public void init() throws ServletException {
        super.init();//调用的是ViewBaseServlet中的init()方法
        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
            //1.创建DocumentBuilderFactory
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            //2.创建DocumentBuilder对象
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
            //3.创建Document对象
            Document document = documentBuilder.parse(inputStream);

            //4.获取所有的bean节点
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE){
                    Element beanElement = (Element)beanNode ;
                    String beanId =  beanElement.getAttribute("id");
                    String className = beanElement.getAttribute("class");
                    Class controllerBeanClass = Class.forName(className);
                    Object beanObj = controllerBeanClass.newInstance() ;
                    beanMap.put(beanId , beanObj) ;
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //设置编码
        request.setCharacterEncoding("UTF-8");
        //假设url是:  http://localhost:8080/pro15/hello.do
        //那么servletPath是:    /hello.do
        // 我的思路是:
        // 第1步: /hello.do ->   hello   或者  /fruit.do  -> fruit
        // 第2步: hello -> HelloController 或者 fruit -> FruitController
        String servletPath = request.getServletPath();
        servletPath = servletPath.substring(1);
        int lastDotIndex = servletPath.lastIndexOf(".do") ;
        servletPath = servletPath.substring(0,lastDotIndex);

        Object controllerBeanObj = beanMap.get(servletPath);

        String operate = request.getParameter("operate");
        if(StringUtil.isEmpty(operate)){
            operate = "index" ;
        }

        try {
            Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
            for(Method method : methods){
                if(operate.equals(method.getName())){
                //1.统一获取请求参数
                    //1-1.获取当前方法的参数,返回参数数组
                    Parameter[] parameters = method.getParameters();
                    //1-2.parameterValues 用来承载参数的值
                    Object[] parameterValues = new Object[parameters.length];
                    for (int i = 0; i < parameters.length; i++) {
                        Parameter parameter = parameters[i];
                        String parameterName = parameter.getName() ;
                        //如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
                        if("request".equals(parameterName)){
                            parameterValues[i] = request ;
                        }else if("response".equals(parameterName)){
                            parameterValues[i] = response ;
                        }else if("session".equals(parameterName)){
                            parameterValues[i] = request.getSession() ;
                        }else{
                            //从请求中获取参数值(除上述三种情况)
                            String parameterValue = request.getParameter(parameterName);
                            String typeName = parameter.getType().getName();

                            Object parameterObj = parameterValue ;

                            if(parameterObj!=null) {
                                if ("java.lang.Integer".equals(typeName)) {
                                    parameterObj = Integer.parseInt(parameterValue);
                                }
                            }

                            parameterValues[i] = parameterObj ;
                        }
                    }
                    //2.controller组件中的方法调用
                    method.setAccessible(true);
                    Object returnObj = method.invoke(controllerBeanObj,parameterValues);

                    //3.视图处理
                    String methodReturnStr = (String)returnObj ;
                    if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
                        String redirectStr = methodReturnStr.substring("redirect:".length());
                        response.sendRedirect(redirectStr);
                    }else{
                        super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
                    }
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

优点:

  • 通过反射统一处理了参数

  • 不用每个模块都需要写一次反射代码


# 3.Service层的诞生

之前我们是 FruitController 直接调用 FruitDAO 的,当需要多个功能的时候要频繁的调用多个 DAO ; 现在我们要中间加一层服务层,FruitController 和 FruitService 通信,FruitService 调用 FruitDAO(实际公司中业务层体量很大)。


# 4.IOC 控制反转和DI 依赖注入的实现

1.之前在 Servlet 中,我们创建 service 对象 , FruitService fruitService = new FruitServiceImpl(); 2.这句话如果出现在 servlet 中的某个方法内部,那么这个 fruitService 的作用域(生命周期)应该就是这个方法级别;(这个方法结束,这个对象的生命周期就结束了) 3。如果这句话出现在 servlet 的类中,也就是说 fruitService 是一个成员变量,那么这个 fruitService 的作用域(生命周期)应该就是这个 servlet 实例级别 4。我们如果在 applicationContext.xml 中定义这个 fruitService,然后通过解析 XML,产生 fruitService 实例,存放在 beanMap 中,这个 beanMap 在一个 BeanFactory 中 5.因此,我们转移(改变)了之前的 service 实例、dao 实例等等他们的生命周期,控制权从程序员转移到 BeanFactory(IOC容器),这个现象我们称之为控制反转。 6.之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();那么,控制层和 service 层存在耦合。 7.之后,我们需要对各层之间进行解耦操作,将代码修改成 FruitService fruitService = null; 8.然后,在配置文件中配置:

<bean id="fruit" class="FruitController">
     <property name="fruitService" ref="fruitService"/>
</bean>
1
2
3

9.使用反射技术将 FruitController 需要的 fruitService 注入进去,而不是之前主动去获取的方式,这就叫依赖注入。

配置文件:

<beans>
    <bean id="fruitDAO" class="com.atguigu.fruit.dao.impl.FruitDAOImpl"/>
    <bean id="fruitService" class="com.atguigu.fruit.service.impl.FruitServiceImpl">
        <!-- property标签用来表示属性;name表示属性名;ref表示引用其他bean的id值-->
        <property name="fruitDAO" ref="fruitDAO"/>
    </bean>
    <bean id="fruit" class="com.atguigu.fruit.controllers.FruitController">
        <property name="fruitService" ref="fruitService"/>
    </bean>
</beans>
1
2
3
4
5
6
7
8
9
10

BeanFactory 接口实现:

public interface BeanFactory {
    Object getBean(String id);
}
1
2
3

实现BeanFactory接口:

public class ClassPathXmlApplicationContext implements BeanFactory {

    private Map<String,Object> beanMap = new HashMap<>();
    private String path = "applicationContext.xml" ;
    public ClassPathXmlApplicationContext(){
        this("applicationContext.xml");
    }
    public ClassPathXmlApplicationContext(String path){
        if(StringUtil.isEmpty(path)){
            throw new RuntimeException("IOC容器的配置文件没有指定...");
        }
        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path);
            //1.创建DocumentBuilderFactory
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            //2.创建DocumentBuilder对象
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
            //3.创建Document对象
            Document document = documentBuilder.parse(inputStream);

            //4.获取所有的bean节点把他们放进beanmap中
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE){
                    Element beanElement = (Element)beanNode ;
                    String beanId =  beanElement.getAttribute("id");
                    String className = beanElement.getAttribute("class");
                    Class beanClass = Class.forName(className);
                    //创建bean实例
                    Object beanObj = beanClass.newInstance() ;
                    //将bean实例对象保存到map容器中
                    beanMap.put(beanId , beanObj) ;
                    //到目前为止,此处需要注意的是,bean和bean之间的依赖关系还没有设置
                }
            }
            //5.组装bean之间的依赖关系
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE) {
                    Element beanElement = (Element) beanNode;
                    String beanId = beanElement.getAttribute("id");
                    NodeList beanChildNodeList = beanElement.getChildNodes();
                    for (int j = 0; j < beanChildNodeList.getLength() ; j++) {
                        Node beanChildNode = beanChildNodeList.item(j);
                        if(beanChildNode.getNodeType()==Node.ELEMENT_NODE && "property".equals(beanChildNode.getNodeName())){
                            Element propertyElement = (Element) beanChildNode;
                            String propertyName = propertyElement.getAttribute("name");
                            String propertyRef = propertyElement.getAttribute("ref");
                            //1) 找到propertyRef对应的实例
                            Object refObj = beanMap.get(propertyRef);
                            //2) 将refObj设置到当前bean对应的实例的property属性上去
                            Object beanObj = beanMap.get(beanId);
                            //通过反射设置对象的 propertyField 属性将它需要的其他类注入进去
                            Class beanClazz = beanObj.getClass();
                            Field propertyField = beanClazz.getDeclaredField(propertyName);
                            propertyField.setAccessible(true);
                            propertyField.set(beanObj,refObj);
                        }
                    }
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object getBean(String id) {
        return beanMap.get(id);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

解释:

1.创建一个 BeanFactory,该实现类负责获取所有 bean 节点,组装 bean 之间的依赖关系。 2.将之前 DispatcherServlet 组件中初始化方法部分拿出来作为一个创建 BeanFactory 的代码,在DispatcherServlet 组件中只需要获取这个 beanfactory 就可以了, beanFactory = new ClassPathXmlApplicationContext();


# 5.过滤器

1)Filter也属于Servlet规范

2)Filter开发步骤:新建实现Filter接口,实现三个方法:init、doFilter、destory

配置Filter,也可用注解@WebFilter,也可使用xml文件<filter><filter-mapping>

3)配置时和servlet一样,也可配置通配符,例如@WebFilter("*.do")

4)过滤器链

  • 如果采用注解方法,拦截顺序是按照全类名先后顺序排列

  • 如果是xml方法配置,按照配置的先后顺序排列

示例:默认情况,只有读到initParams中的参数,才会给encoding赋值,否则默认utf-8

package com.demo.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter(urlPatterns = {"*.do"},initParams =  {@WebInitParam(name = "encoding",value = "utf-8")})
public class GlobalFilter implements Filter {
    private String encoding="utf-8";
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String encodingStr  = filterConfig.getInitParameter("encoding");
        if (encodingStr != null){
            encoding = encodingStr;
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ((HttpServletRequest)servletRequest).setCharacterEncoding(encoding);
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 6.ThreadLocal

# 1.核心定义

ThreadLocal 是 Java 提供的一个线程本地存储工具类,它的核心作用是:为每个使用它的线程创建一个独立的变量副本

# 2. 通俗比喻

  • 没有 ThreadLocal 时:多个线程共用一个 “全局变量”,线程 A 修改后,线程 B 看到的就是修改后的值,会互相干扰;
  • 有 ThreadLocal 时:相当于给每个线程发了一个 “专属储物柜”,线程 A 往自己的柜子里放东西,线程 B 往自己的柜子里放东西,彼此看不到、不干扰,取出的永远是自己放进去的那个值。

# 3.源码解析

set方法

// ThreadLocal的set方法
public void set(T value) {
    // 1. 获取当前执行set方法的线程对象
    Thread t = Thread.currentThread();
    // 2. 获取当前线程t内部的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 3. 如果map存在,以当前ThreadLocal实例(this)为key,存入value
        map.set(this, value);
    else
        // 4. 如果map不存在,为当前线程t创建ThreadLocalMap,并存入第一个键值对
        createMap(t, value);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

get方法

public T get() {
    // 1. 还是先拿当前线程
    Thread t = Thread.currentThread();
    // 2. 还是拿当前线程的ThreadLocalMap(和set时的map是同一个)
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 3. 以当前ThreadLocal实例(this)为key,从map中取值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 4. 没取到就返回初始值
    return setInitialValue();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.监听器

监听大类 监听器接口 核心作用 核心方法 关键特点 / 常用场景
ServletContext(全局上下文) ServletContextListener 监听 ServletContext 的创建 / 销毁(应用启停) 1. contextInitialized ():应用启动执行

2. contextDestroyed ():应用关闭执行
全局唯一,应用级别;用于初始化连接池、加载配置、释放全局资源
ServletContext(全局上下文) ServletContextAttributeListener 监听全局属性的添加 / 删除 / 替换 1. attributeAdded ():添加属性

2. attributeRemoved ():删除属性

3. attributeReplaced ():替换属性
监控全局缓存 / 参数变更;所有用户共享
👤 HttpSession(用户会话) HttpSessionListener 监听 Session 的创建 / 销毁(用户上线 / 下线) 1. sessionCreated ():Session 创建(首次访问)

2. sessionDestroyed ():Session 销毁(超时 / 手动失效)
统计在线用户数、记录用户登录 / 退出时间;每个用户一个 Session
HttpSession(用户会话) HttpSessionAttributeListener 监听 Session 属性的添加 / 删除 / 替换 1. attributeAdded ():添加属性

2. attributeRemoved ():删除属性

3. attributeReplaced ():替换属性
监控用户会话数据(如登录状态、购物车)变更
HttpSession(用户会话) HttpSessionBindingListener 监听对象与 Session 的绑定 / 解绑 1. valueBound ():对象绑定到 Session

2. valueUnbound ():对象从 Session 解绑
无需配置(由被绑定对象实现);对象存入 / 移除 Session 时触发,用于对象初始化 / 清理
HttpSession(用户会话) HttpSessionActivationListener 监听 Session 的钝化 / 活化(序列化 / 反序列化) 1. sessionWillPassivate ():Session 钝化前

2. sessionDidActivate ():Session 活化后
需实现 Serializable 接口;处理服务器内存不足时 Session 的持久化 / 恢复
ServletRequest(单次请求) ServletRequestListener 监听 Request 的创建 / 销毁(请求开始 / 结束) 1. requestInitialized ():请求创建

2. requestDestroyed ():请求销毁
记录请求处理时长、统计接口访问量、统一设置请求编码;每次请求一个 Request
ServletRequest(单次请求) ServletRequestAttributeListener 监听 Request 属性的添加 / 删除 / 替换 1. attributeAdded ():添加属性

2. attributeRemoved ():删除属性

3. attributeReplaced ():替换属性
监控请求级数据传递(如 Controller 向 JSP 传参);请求结束后属性销毁

1.cookie

Http默认无状态的,分不清是哪个用户来进行操作,所以就有了cookie

1)介绍

cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。

服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

2)流程

  • 服务器会 自动 的将 cookie 响应给浏览器。

  • 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。

  • 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。

3)大小:单个 Cookie≤4KB,一个域名下最多存 20 个左右;

4)优缺点

  • 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)

  • 缺点:

    • 移动端APP(Android、IOS)中无法使用Cookie

    • 不安全,用户可以自己禁用Cookie

    • Cookie不能跨域

2.session

但是cookie存储的数据又太小,又是明文传输,太不安全了,于是就有了session

1)流程

  • 获取session

浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session,Session 并分配一个SessionID。

  • 响应Cookie (JSESSIONID)

接下来,服务器端在给浏览器响应数据的时候,它会将 SessionID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 SessionID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。

  • 查找Session

接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。

2) 核心特点

  • 生命周期:默认 30 分钟超时(可配置),浏览器关闭后 Cookie 失效,但 Session 仍在服务器保留至超时;
  • 安全性:比 Cookie 高(信息存在服务器),但依赖 Cookie 传递 SessionID,若 SessionID 被窃取(如 CSRF 攻击),仍有风险;
  • 核心问题:分布式系统中,Session 存在服务器本地,会导致 “会话共享” 问题(用户请求到不同服务器,找不到 Session)。

3)优缺点

  • 优点:Session是存储在服务端的,安全

  • 缺点:

    • 服务器集群环境下无法直接使用Session

    • 移动端APP(Android、IOS)中无法使用Cookie

    • 用户可以自己禁用Cookie

    • Cookie不能跨域

3.token

由于session不好很好的处理跨域和集群问题,所以token出现了

1)流程

  • 用户登录,服务器分发token

  • 客户端保存token

  • 再次请求时,客户端携带token访问

  • 服务器解析token,有效未过期就放行

2)核心特点

  • 存储位置:客户端(LocalStorage/Cookie/ 内存),服务器不存储 Token(JWT);
  • 无状态:服务器无需保存 Token,仅通过加密算法验证有效性,适合分布式系统;
  • 安全性:可签名 / 加密(如 JWT 的 HS256/RSA),防篡改;支持跨域(CORS),适合前后端分离 / 移动端;
  • 核心问题:Token 一旦签发,无法主动作废(除非服务器维护黑名单例如redis);Token 过长会增加请求开销。

3)优缺点

  • 优点:

    • 支持PC端、移动端

    • 解决集群环境下的认证问题

    • 减轻服务器的存储压力(无需在服务器端存储)

  • 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)

对比表

维度 Cookie Session Token(如 JWT)
存储位置 客户端(浏览器) 服务器端 客户端(任意)
状态性 无状态(仅存数据) 有状态(服务器存会话) 无状态(服务器不存)
安全性 低(明文,易窃取) 中(服务器存储,依赖 Cookie) 高(加密签名,防篡改)
大小限制 ≤4KB 无(服务器内存决定) 无(但过长影响性能)
跨域支持 差(同源策略限制) 差(依赖 Cookie) 好(请求头携带)
分布式支持 无(仅客户端) 差(需会话共享) 好(无状态)
核心用途 传递 SessionID、存非敏感信息 存储用户敏感会话信息 前后端分离 / 跨域 / 分布式认证
最近更新: 9/19/2026, 1:27:08 PM
编程NOTE   |