【SpringMVC】9—底层原理

news2024/11/15 23:47:32

⭐⭐⭐⭐⭐⭐
Github主页👉https://github.com/A-BigTree
笔记链接👉https://github.com/A-BigTree/Code_Learning
⭐⭐⭐⭐⭐⭐

如果可以,麻烦各位看官顺手点个star~😊

如果文章对你有所帮助,可以点赞👍收藏⭐支持一下博主~😆


文章目录

  • 9 底层原理
    • 9.1 启动过程
      • 9.1.1 初始化操作调用路线
        • 类和接口之间的关系
        • 调用路线图
      • 9.1.2 IOC容器创建
      • 9.1.3 IOC容器对象存入应用域
      • 9.1.4 请求映射初始化
      • 9.1.5 总结
    • 9.2 请求处理过程
      • 9.2.1 总体阶段
        • 流程描述
        • 核心代码
      • 9.2.2 调用前阶段
        • 建立调用链
        • 调用拦截器`preHandle()`
        • 注入请求参数
    • 9.3 `ContextLoaderListener`
      • 9.3.1 问题
      • 9.3.2 配置`ContextLoaderListener`
        • 创建spring-persist.xml并配置
        • `ContextLoaderListener`
        • `ContextLoader`
      • 9.3.3 两个IOC之间的关系
      • 9.3.4 两个容器之间访问关系
      • 9.3.5 重复创建问题
        • 问题
        • 解决方法1
        • 解决方法2

9 底层原理

9.1 启动过程

9.1.1 初始化操作调用路线

类和接口之间的关系

在这里插入图片描述

调用路线图

调用线路图所示是方法调用的顺序,但是实际运行的时候本质上都是调用DispatcherServlet对象的方法。包括这里涉及到的接口的方法,也不是去调用接口中的『抽象方法』。毕竟抽象方法是没法执行的。抽象方法一定是在某个实现类中有具体实现才能被调用。

而对于最终的实现类:DispatcherServlet来说,所有父类的方法最后也都是在DispatcherServlet对象中被调用的。

在这里插入图片描述

9.1.2 IOC容器创建

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
  Class<?> contextClass = getContextClass();
  if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
    throw new ApplicationContextException(
        "Fatal initialization error in servlet with name '" + getServletName() +
        "': custom WebApplicationContext class [" + contextClass.getName() +
        "] is not of type ConfigurableWebApplicationContext");
  }
    
    // 通过反射创建 IOC 容器对象
  ConfigurableWebApplicationContext wac =
      (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

  wac.setEnvironment(getEnvironment());
    
    // 设置父容器
  wac.setParent(parent);
  String configLocation = getContextConfigLocation();
  if (configLocation != null) {
    wac.setConfigLocation(configLocation);
  }
  
  // 配置并且刷新:在这个过程中就会去读XML配置文件并根据配置文件创建bean、加载各种组件
  configureAndRefreshWebApplicationContext(wac);

  return wac;
}

9.1.3 IOC容器对象存入应用域

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext initWebApplicationContext() {
  WebApplicationContext rootContext =
      WebApplicationContextUtils.getWebApplicationContext(getServletContext());
  WebApplicationContext wac = null;

  if (this.webApplicationContext != null) {
    wac = this.webApplicationContext;
    if (wac instanceof ConfigurableWebApplicationContext) {
      ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
      if (!cwac.isActive()) {
        if (cwac.getParent() == null) {
          cwac.setParent(rootContext);
        }
        configureAndRefreshWebApplicationContext(cwac);
      }
    }
  }
  if (wac == null) {
    wac = findWebApplicationContext();
  }
  if (wac == null) {
        // 创建 IOC 容器
    wac = createWebApplicationContext(rootContext);
  }

  if (!this.refreshEventReceived) {
    synchronized (this.onRefreshMonitor) {
      onRefresh(wac);
    }
  }

  if (this.publishContext) {
    // 获取存入应用域时专用的属性名
    String attrName = getServletContextAttributeName();
        
        // 存入
    getServletContext().setAttribute(attrName, wac);
  }

  return wac;
}

看到这一点的意义:SpringMVC 有一个工具方法,可以从应用域获取 IOC 容器对象的引用。

工具类:org.springframework.web.context.support.WebApplicationContextUtils

工具方法:getWebApplicationContext()

@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
  return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

作用:将来假如我们自己开发时,在IOC容器之外需要从IOC容器中获取bean,那么就可以通过这个工具方法获取IOC容器对象的引用。IOC容器之外的场景会有很多,比如在一个我们自己创建的Filter中。

9.1.4 请求映射初始化

FrameworkServlet.createWebApplicationContext()configureAndRefreshWebApplicationContext()wac.refresh()→触发刷新事件 → org.springframework.web.servlet.DispatcherServlet.initStrategies()org.springframework.web.servlet.DispatcherServlet.initHandlerMappings()

9.1.5 总结

整个启动过程我们关心如下要点:

  • DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度;
  • DispatcherServlet 的父类是 FrameworkServlet
    • FrameworkServlet 负责框架本身相关的创建和初始化;
    • DispatcherServlet 负责请求处理相关的初始化;
  • FrameworkServlet 创建 IOC 容器对象之后会存入应用域;
  • FrameworkServlet 完成初始化会调用 IOC 容器的刷新方法;
  • 刷新方法完成触发刷新事件,在刷新事件的响应函数中,调用 DispatcherServlet 的初始化方法;
  • DispatcherServlet 的初始化方法中初始化了请求映射等;

9.2 请求处理过程

9.2.1 总体阶段

流程描述

  • 目标handler方法执行
    • 建立调用链,确定整个执行流程
    • 拦截器的preHandle()方法
    • 注入请求参数
    • 准备目标handler方法所需所有参数
  • 调用目标handler方法
  • 目标handler方法执行
    • 拦截器的postHandle()方法
    • 渲染视图
    • 拦截器的afterCompletion()方法

核心代码

整个请求处理过程都是doDispatch()方法在宏观上协调和调度,把握了这个方法就理解了SpringMVC总体上是如何处理请求的。

所在类:DispatcherServlet

所在方法:doDispatch()

核心方法中的核心代码:

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

9.2.2 调用前阶段

建立调用链

相关组件:

全类名:org.springframework.web.servlet.HandlerExecutionChain

在这里插入图片描述

拦截器索引默认是 -1,说明开始的时候,它指向第一个拦截器前面的位置。每执行一个拦截器,就把索引向前移动一个位置。所以这个索引每次都是指向当前拦截器。所以它相当于拦截器的指针。

对应操作:

所在类:org.springframework.web.servlet.handler.AbstractHandlerMapping

所在方法:getHandlerExecutionChain()

关键操作:

  • 把目标handler对象存入
  • 把当前请求要经过的拦截器存入

在这里插入图片描述

结论:调用链是由拦截器和目标handler对象组成的。

调用拦截器preHandle()

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

  • 具体调用细节:正序调用;
  • 所在类:org.springframework.web.servlet.HandlerExecutionChain
  • 所在方法:applyPreHandle()

在这里插入图片描述

从这部分代码我们也能看到,为什么拦截器中的 preHandle() 方法通过返回布尔值能够控制是否放行。

  • 每一个拦截器的 preHandle() 方法都返回 trueapplyPreHandle() 方法返回 true,被取反就不执行 if 分支,继续执行后续操作,这就是放行;
  • 任何一个拦截器的 preHandle() 方法返回 falseapplyPreHandle() 方法返回 false,被取反执行 if 分支,return,导致 doDispatch() 方法结束,不执行后续操作,就是不放行。

注入请求参数

接口:org.springframework.web.servlet.HandlerAdapter

作用:字面含义是适配器的意思,具体功能有三个

  • 将请求参数绑定到实体类对象中
  • 给目标 handler 方法准备所需的其他参数,例如:
    • Model、ModelMap、Map……
    • 原生 Servlet API:request、response、session……
    • BindingResult
    • @RequestParam注解标记的零散请求参数
    • @PathVariable注解标记的路径变量
  • 调用目标 handler 方法

所以 HandlerAdapter 这个适配器是将底层的 HTTP 报文、原生的 request 对象进行解析和封装,『适配』到我们定义的 handler 方法上。

通过反射给对应属性注入请求参数应该是下面的过程:

  • 获取请求参数名称;
  • 将请求参数名称首字母设定为大写;
  • 在首字母大写后的名称前附加set,得到目标方法名;
  • 通过反射调用setXxx()方法;

9.3 ContextLoaderListener

9.3.1 问题

目前情况:DispatcherServlet 加载 spring-mvc.xml,此时整个 Web 应用中只创建一个 IOC 容器。将来整合Mybatis、配置声明式事务,全部在 spring-mvc.xml 配置文件中配置也是可以的。可是这样会导致配置文件太长,不容易维护。

所以想到把配置文件分开:

  • 处理浏览器请求相关:spring-mvc.xml 配置文件
  • 声明式事务和整合Mybatis相关:spring-persist.xml 配置文件

配置文件分开之后,可以让 DispatcherServlet 加载多个配置文件。例如:

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-*.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

如果希望这两个配置文件使用不同的机制来加载:

  • DispatcherServlet 加载 spring-mvc.xml 配置文件:它们和处理浏览器请求相关
  • ContextLoaderListener 加载 spring-persist.xml 配置文件:不需要处理浏览器请求,需要配置持久化层相关功能

此时会带来一个新的问题:在一个 Web 应用中就会出现两个 IOC 容器

  • DispatcherServlet 创建一个 IOC 容器
  • ContextLoaderListener 创建一个 IOC 容器

注意:本节我们探讨的这个技术方案并不是『必须』这样做,而仅仅是『可以』这样做。

9.3.2 配置ContextLoaderListener

创建spring-persist.xml并配置

<!-- 通过全局初始化参数指定 Spring 配置文件的位置 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-persist.xml</param-value>
</context-param>
 
<listener>
    <!-- 指定全类名,配置监听器 -->
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

ContextLoaderListener

方法名执行时机作用
contextInitialized()Web 应用启动时执行创建并初始化 IOC 容器
contextDestroyed()Web 应用卸载时执行关闭 IOC 容器

ContextLoader

ContextLoader 类是 ContextLoaderListener 类的父类。

9.3.3 两个IOC之间的关系

两个组件分别创建的 IOC 容器是父子关系。

  • 父容器:ContextLoaderListener 创建的 IOC 容器;
  • 子容器:DispatcherServlet 创建的 IOC 容器;

父子关系是如何决定的?

  • Tomcat 在读取 web.xml 之后,加载组件的顺序就是监听器、过滤器、Servlet。
  • ContextLoaderListener初始化时如果检查到有已经存在的根级别 IOC 容器,那么会抛出异常。
  • DispatcherServlet 创建的 IOC 容器会在初始化时先检查当前环境下是否存在已经创建好的 IOC 容器。
    • 如果有:则将已存在的这个 IOC 容器设置为自己的父容器
    • 如果没有:则将自己设置为 root 级别的 IOC 容器

9.3.4 两个容器之间访问关系

子容器中的 EmpController 装配父容器中的 EmpService 能够正常工作。说明子容器可以访问父容器中的bean。

分析:“子可用父,父不能用子”的根本原因是子容器中有一个属性 getParent() 可以获取到父容器这个对象的引用。

源码依据:

  • AbstractApplicationContext 类中,有 parent 属性;
  • AbstractApplicationContext 类中,有获取 parent 属性的 getParent() 方法;
  • 子容器可以通过 getParent() 方法获取到父容器对象的引用;
  • 进而调用父容器中类似 “getBean()” 这样的方法获取到需要的 bean 完成装配;
  • 而父容器中并没有类似 “getChildren()“ 这样的方法,所以没法拿到子容器对象的引用;

在这里插入图片描述

9.3.5 重复创建问题

问题

  • 浪费内存空间

  • 两个 IOC 容器能力是不同的

    • spring-mvc.xml:仅配置和处理请求相关的功能。所以不能给 service 类附加声明式事务功能。

      结论:基于 spring-mvc.xml 配置文件创建的 EmpService 的 bean 不带有声明式事务的功能

      影响:DispatcherServlet 处理浏览器请求时会调用自己创建的 EmpController,然后再调用自己创建的EmpService,而这个 EmpService 是没有事务的,所以处理请求时没有事务功能的支持。

    • spring-persist.xml:配置声明式事务。所以可以给 service 类附加声明式事务功能。

      结论:基于 spring-persist.xml 配置文件创建的 EmpService 有声明式事务的功能

      影响:由于 DispatcherServlet 的 IOC 容器会优先使用自己创建的 EmpController,进而装配自己创建的EmpService,所以基于 spring-persist.xml 配置文件创建的有声明式事务的 EmpService 用不上。

解决方法1

让两个配置文件配置自动扫描的包时,各自扫描各自的组件。

  • SpringMVC 就扫描 XxxHandlerXXXController
  • Spring 扫描 XxxServiceXxxDao

解决方法2

如果由于某种原因,必须扫描同一个包,确实存在重复创建对象的问题,可以采取下面的办法处理。

  • spring-mvc.xml 配置文件在整体扫描的基础上进一步配置:仅包含被 @Controller 注解标记的类。
  • spring-persist.xml 配置在整体扫描的基础上进一步配置:排除被 @Controller 注解标记的类。

具体spring-mvc.xml配置文件中的配置方式如下:

<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 为了解决重复创建对象的问题,需要进一步制定扫描组件时的规则 -->
<!-- 目标:『仅』包含@Controller注解标记的类 -->
<!-- use-default-filters="false"表示关闭默认规则,表示什么都不扫描,此时不会把任何组件加入IOC容器;
        再配合context:include-filter实现“『仅』包含”效果 -->
<context:component-scan base-package="com.atguigu.spring.component" use-default-filters="false">

    <!-- context:include-filter标签配置一个“扫描组件时要包含的类”的规则,追加到默认规则中 -->
    <!-- type属性:指定规则的类型,根据什么找到要包含的类,现在使用annotation表示基于注解来查找 -->
    <!-- expression属性:规则的表达式。如果type属性选择了annotation,那么expression属性配置注解的全类名 -->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

具体spring-persist.xml配置文件中的配置方式如下:

<!-- 两个Spring的配置文件扫描相同的包 -->
<!-- 在默认规则的基础上排除标记了@Controller注解的类 -->
<context:component-scan base-package="com.atguigu.spring.component">

    <!-- 配置具体排除规则:把标记了@Controller注解的类排除在扫描范围之外 -->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/410752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于springboot和ajax的简单项目 02 代码部分实现,xml文件sql语句优化 (中)

上次说到了log/log_list.html的doGetObjects()&#xff0c;其中有doFindPageObjects&#xff08;&#xff09;方法。 找到全部的日志对象&#xff0c;并且输出到div上。这里是后台的代码。 01.pojo对象&#xff0c;这里需要序列化保存。序列化的作用是保存对象到内存缓存中&am…

腾讯38K测试良心分享,熬夜整理10万字详细软件测试面试笔记

国内的互联网面试&#xff0c;恐怕是现存的、最接近科举考试的制度。 这是由于互联网IT行业的求职者太多了&#xff0c;如果考察的是清一溜的算法题和设计题&#xff0c;那么会要求面试官有极高的技术水平&#xff0c;还要花大量的时间成本和精力。 所以&#xff0c;八股文面…

普通人如何使用ChatGPT接单挣钱-第一弹

嘿&#xff0c;我是灵牧&#xff0c;今天这一期给大家带来的是普通人如何使用chatGPT挣钱的第一期 第一弹&#xff1a;通过GPT写一个Tkinter数独案例赚取&#xffe5;50 事情原委&#xff1a; 今天在接单群里看到一个Tkinter写数独的需求案例&#xff0c;想要加一个独立的弹…

Golang每日一练(leetDay0031)

目录 91. 解码方法 Decode Ways &#x1f31f;&#x1f31f; 92. 反转链表 II Reverse Linked List II &#x1f31f;&#x1f31f; 93. 复原 IP 地址 Restore IP Addresses &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练…

pdf转excel表格怎么做?这3种方法简单又方便

在日常工作和生活中&#xff0c;我们经常需要转换不同格式的文件&#xff0c;比如将PDF转换为Excel表格。虽然转换过程看似简单&#xff0c;但实际上要想转换好&#xff0c;选择一款专业的PDF转换器是非常必要的。今天&#xff0c;我将为大家介绍几款值得推荐的PDF转换器&#…

一文全解经典机器学习算法之线性回归(关键词:回归分析、最小二乘法、极大似然估计、梯度下降法、逻辑回归、对数几率、线性判别分析)

文章目录一&#xff1a;线性回归基本概念&#xff08;1&#xff09;回归分析&#xff08;2&#xff09;线性回归二&#xff1a;线性回归确定参数的方法&#xff08;1&#xff09;最小二乘法A&#xff1a;代数求法B&#xff1a;矩阵求法&#xff08;主要使用&#xff09;&#x…

国产大模型狂飙,谁能率先做出第一个中国版GPT

热火烹油的大模型赛道打起了“嘴仗”。 搜狗前CEO王小川评价百度创始人李彦宏的采访发言称&#xff1a;“你们采访的可能是平行世界的他&#xff0c;不是我们这个世界里的。” 而针对王小川的评论&#xff0c;百度集团副总裁、搜索平台负责人肖阳又回应道&#xff1a;“王小…

JavaSE补充 | 网络编程

目录 一&#xff1a;网络编程 1. 网络编程概述 2. 网络通信要素 3. 传输层协议&#xff1a;TCP 与 UDP 协议 4. 网络编程 API 5. TCP网络编程 6. UDP 网络编程 7. URL编程 一&#xff1a;网络编程 1. 网络编程概述 Java是 Internet 上的语言&#xff0c;它从语言级上提…

【redis】单线程 VS 多线程(入门)

【redis】单线程 VS 多线程&#xff08;入门&#xff09; 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#…

Python之数据库操作(连接数据库,增删改查操作,易错点理解)

文章目录 前言一、Python之数据库操作二、pymysql 安装三、pymysql 包引入 连接数据库创建游标执行sql数据 - 增删改查要获取查询结果数据关闭游标&#xff0c;关闭数据库连接总结前言 记录&#xff1a;Python操作数据库的步骤&#xff0c;不容易理解的地方。 一、Python之数据…

开局一张图,构建神奇的 CSS 效果

假设&#xff0c;我们有这样一张 Gif 图&#xff1a; 利用 CSS&#xff0c;我们尝试来搞一些事情。 图片的 Glitch Art 风 在这篇文章中 --CSS 故障艺术&#xff0c;我们介绍了利用混合模式制作一种晕眩感觉的视觉效果。有点类似于抖音的 LOGO。 像是这样&#xff1a; 假设…

14、DRF实战总结:获取Django请求路径的方法以及各自的区别

Django项目开发中经常需要在视图中获取用户当前请求url的地址&#xff0c;然后进行跳转或判断操作&#xff0c;比如是否在url黑白名单里。Django提供了多种获取请求路径的实现方式&#xff0c;比如request.path, request.path_info, request.get_full_path和request.build_abso…

【Python】【进阶篇】十六、Python爬虫的浏览器实现抓包

目录十六、Python爬虫的浏览器实现抓包16.1 控制台界面16.1.1 NetWork16.1.2 Sources16.1.3 Console16.1.4 Application16.2 数据包抓取16.3 看变化规律十六、Python爬虫的浏览器实现抓包 几乎所有浏览器都提供了抓取数据包的功能&#xff0c;因为浏览器为抓包提供了一个专门的…

【MQTT协议】使用c++实现mqtt协议(Mosquitto源码编译)

目录MQTT协议简介发布/订阅模式简介MQTT协议与发布/订阅模式的联系基于Mosquitto实现的MQTT服务器Mosquitto安装MQTT协议简介 MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的、基于发布/订阅模式的消息传输协议&#xff0c;其最初由IBM开发&…

(链表专题) 445. 两数相加 II ——【Leetcode每日一题】

445. 两数相加 II 给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数字都不会以零开头。 示例1&#xff1a; 输入&#xff1a;l1 [7,2,…

什么是时间复杂度和空间复杂度

什么是时间复杂度和空间复杂度 &#x1f355;博客主页&#xff1a;️自信不孤单 &#x1f36c;文章专栏&#xff1a;数据结构与算法 &#x1f35a;代码仓库&#xff1a;破浪晓梦 &#x1f36d;欢迎关注&#xff1a;欢迎大家点赞收藏关注 文章目录什么是时间复杂度和空间复杂度1…

【服务器数据恢复】 EXT4文件系统下KVM虚拟机数据恢复案例

服务器数据恢复环境&#xff1a; Linux系统服务器&#xff0c;EXT4文件系统&#xff0c;部署KVM虚拟机。 服务器故障&#xff1a; 服务器上的KVM虚拟机被误操作删除&#xff0c;每台虚拟机包含一个qcow2格式的磁盘文件和一个raw格式的磁盘文件&#xff0c;需要恢复raw格式的磁…

大数据-玩转数据-oracle创建dblink及应用

一、创建DBLINK的应用场景 oracle在进行跨库访问时&#xff0c;可以通过创建dblink实现。 二、创建DBLINK应用场景 在tnsnames.ora中配置两个数据库别名&#xff1a;orcl(用户名&#xff1a;wangyong 密码&#xff1a;1988)、orcl2(用户名&#xff1a;wangyong 密码&#xf…

第5章 uniapp开发ImoocBlog

收获&#xff1a; 1.微信小程序 2.uni-app开发慕课热搜项目 3.构建企业级项目的编程思维 4.上线可商用的企业项目 第5章 uniapp开发ImoocBlog uniapp开发ImoocBlog 5-1&#xff1a;开篇 经过前面四个章节&#xff0c;我们已经完成了 微信小程序 的学习。那么从这一章开始我…

【微服务笔记10】微服务组件之Hystrix实现服务降级和服务熔断

这篇文章&#xff0c;主要介绍微服务组件之Hystrix实现服务降级和服务熔断。 目录 一、服务降级 1.1、什么是服务降级 1.2、实现服务降级 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;编写Service层代码 &#xff08;3&#xff09;编写Controller层代码…