SpringMVC


 

1、SpringMVC

  1. 什么是MVC

    MVC是一种软件的构想将软 件按照模型、视图、控制器来划分

    M:Model 模型, 指的是JavaBean 实体Bean和业务处理Bean(实体类和Dao、Service)

    V:View 视图, 指Html或者JSP等页面

    C:Controller 控制器,值工程中的Servlet,作用接收请求、响应请求

    MVC工作流程:

    用户通过视图发生请求到服务器,在服务器中被Controller接收,Controller调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller在根据请求处理的结果找到相应的View视图层,渲染数据后最终响应给游览器

2.什么是SpringMVC

SpringMVC是Spring的一个后续产品,是Spring的一个子模块,是Spring为表述层开发提供的一整套完备的解决方案

3.SpringMVC的特点

  • Spring家族原生产品,与IOC容器等基础设施无缝对接
  • 基于原生的Servlet,通过功能强大的DispatchServlet,对请求和响应进行统一处理
  • 代码清晰简洁
  • 性能卓越

2、HelloWorld

web.xml配置

spring中要求配置文件位于recesouces下,则可以在web.xml的servlet配置中,配置init-param指定配置文件的位置,如果没有配置init-param,则需要在WEB-INF下创建名称为[servlet-name]-servlet.xml的配置文件

<!--    1.配置控制器 DispatcherServlet-->
<servlet>
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <!-- 指定SpringMVC配置文件的位置 -->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springMVC.xml</param-value>
    </init-param>
    <!-- 指定当前servlet的启动顺序 -->
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <!-- "/" 所有的请求(不包括.jsp),交由控制器DispatcherServlet去处理 -->
    <!-- "/*" 所有的请求(包括.jsp) -->
    <url-pattern>/</url-pattern>
</servlet-mapping>
springMVC.xml
<!--    1.组件加载-->
<context:component-scan base-package="com.potato.controller"/>
<!--    2.静态资源处理  需要和 <mvc:annotation-driven />   一起使用-->
<mvc:default-servlet-handler />
<!--    3.开启mvc注解驱动-->
<mvc:annotation-driven />
<!--    4.视图配置-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/static/html/" />
    <property name="suffix" value=".jsp" />
</bean>
HelloController.java
@Controller
@RequestMapping("/test")
public class HelloController {

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

index.jsp

在/WEB-INF/static/html/下创建index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>首页</title>
</head>
<body>
<h1>首页</h1>
<a href="${pageContext.request.contextPath}/hello">跳转</a>
</body>
</html>

配置启动Tomacat 即可完成web项目的访问

3、 @ReuqestMapping 注解

用于标识类或者方法
@Controller
@RequestMapping("/test")
public class HelloController {

    @RequestMapping("/index")  //访问路径为 /test/index
    public String index() {
        return "index";
    }

    @RequestMapping("/error")  //访问路径为 /test/error
    public String error() {
        return "error";
    }
}
@ReuqestMapping 属性值:
  • value: 通过请求路径匹配请求映射。可以是一个数组,表示该请求映射可以处理多个请求地址

  • method:通过请求方式匹配请求映射,请求方式 get/post/delete/put

  • params:通过请求参数匹配请求映射

  • headers:通过请求头匹配请求映射


@Controller
@RequestMapping("/test")
public class HelloController {
 	// value
    // 可以处理 /test/index 和 /test/main  请求
    @RequestMapping(value = {"index", "main"})
    public String index() {
        return "index";
    }

    // method
    // 只能处理 /test/index2  并且 请求方式为GET的请求
    @RequestMapping(value = "index2", method = RequestMethod.GET)
    public String index2() {
        return "index";
    }

    // params
    // 只能处理 /test/index3  并且 携带username参数,password参数的值必须是123456,age参数不能等于20,不允许有sex参数
    @RequestMapping(value = "index3", params = {"username", "password=123456", "age!=20", "!sex"})
    public String index3() {
        return "index";
    }

    //headers
    // 只能处理 /test/index4  并且 请求头的Host值必须为localhost:8080
    @RequestMapping(value = "index3", headers = {"Host=localhost:8080"})
    public String index4() {
        return "index";
    }

}
ant风格,路径规则、匹配

“?” :表示任意单个字符

“*” :表示任意0个或多个字符

“**” :表示任意一层或多层目录

//注意:在使用 ”**“时,只能使用 /**/xxx 的形式
// 表示无论是 /aat/test 或者 /abt/test、/act/test 等等请求,都可以匹配到当前映射方法
@RequestMapping("/a?t/test")  
public String test() {
    return "index";
}
restful风格,路径占位符、传值

路径占位符常用于restful风格,在响应的@RequestMapping注解的value属性中通过占位符{xxx}表示要传输的数据,在通过@PathVarlable注解获取占位符中的值

/**  3. restful风格 */
//在@RequestMapping注解的value属性中通过占位符{xxx}表示要传输的数据,在通过@PathVarlable注解获取占位符中的值
@RequestMapping("/restful/{id}/{pageIndex}")
public String restful(@PathVariable("id") String id,@PathVariable("pageIndex") Integer pageIndex) {
    System.out.println("id:"+id);
    System.out.println("pageIndex:"+pageIndex);
    return "index";
}
派生注解

@GetMapping、@PostMapping、@DeleteMapping、@PutMapping

4、获取请求数据

原生ServletAPI获取请求参数
控制器方法形参获取请求参数
@RequestParam
@RequesHeader
@CookieValue
通过POJO获取请求参数

解决请求乱码问题

/**
* 1.原生ServletAPI获取请求参数
*/
@RequestMapping("index")
public String index(HttpServletRequest request) {
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    System.out.println("username:" + username);
    System.out.println("password:" + password);
    return "index";
}

/**
* 2.控制器方法形参获取请求参数,请求参数名和形参名 必须要一致
*/
@RequestMapping("index2")
public String index2(String username, String password) {
    System.out.println("username:" + username);
    System.out.println("password:" + password);
    return "index";
}

/**
* 3.@RequestParam, 请求参数名和形参名 不一致
* value/name: 请求参数的名称,name和value两个属性基本是等价的,
* required:当前参数是否必须存在
* defaultValue:当前参数的默认值
*/
@RequestMapping(value = "index3")
public String index3(@RequestParam("user_name") String username) {
    System.out.println("username:" + username);
    return "index";
}
/**
* 4.@RequestHeader 获取请求头信息
* 和@RequestParam 一样的使用方法
*/
@RequestMapping(value = "index4")
public String index4(@RequestHeader("Host") String host) {
    System.out.println("host:" + host);
    return "index";
}

/**
* 5.@CookieValue 获取cookie数据
* 和@RequestParam 一样的使用方法
*/
@RequestMapping(value = "index5")
public String index5(@CookieValue("JSESSIONID") String JSESSIONID) {
    System.out.println("JSESSIONID:" + JSESSIONID);
    return "index";
}

/**
* 6.通过POJO获取请求参数
* 请求参数的名称和POJO类对象的属性名 要一致
*/
@RequestMapping(value = "index6")
public String index6(User user) {
    System.out.println(user);
    return "index";
}
解决乱码

get请求乱码 由tomcat照成,修改tomcat的配置文件

post乱码 配置CharacterEncodingFilter

<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>
        <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>

4、域对象数据共享

1.ServletAPI向request域对象共享数据
2.ModelAndView向request域对象共享数据
3.Model向request域对象共享数据
4.Map向request域对象共享数据
5.ModelMap向request域对象共享数据
6.Model、ModelMap、Map的关系
7.向session域对象共享数据
8.向application域对象共享数据
//1.ServletAPI向request域对象共享数据
@RequestMapping("/testServletApi")
public String testServletApi(HttpServletRequest request) {
    request.setAttribute("testRequestScope", "向域对象中共享的数据");
    return "index";
}

//2.ModelAndView向request域对象共享数据
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(ModelAndView mav) {
    // 向域对象中添加数据
    mav.addObject("testRequestScope", "向域对象中共享的数据");
    // 跳转的视图页面
    mav.setViewName("index");
    return mav;
}

//3.Model向request域对象共享数据
@RequestMapping("/testModel")
public String testModel(Model model) {
    model.addAttribute("testRequestScope", "向域对象中共享的数据");
    return "index";
}

//4.Map向request域对象共享数据
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map) {
    map.put("testRequestScope", "向域对象中共享的数据");
    return "index";
}

//5.ModelMap向request域对象共享数据
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap) {
    modelMap.addAttribute("testRequestScope", "向域对象中共享的数据");
    return "index";
}

//6.Model、ModelMap、Map的关系  都是BindingAwareModelMap
//    public interface Model
//    public class ModelMap extends LinkedHashMap<String, Object>
//    public class ExtendedModelMap extends ModelMap implements Model
//    public class BindingAwareModelMap extends ExtendedModelMap

//7.向session域对象共享数据
@RequestMapping("/testSessionScopeByServletApi")
public String testSessionScopeByServletApi(HttpSession session) {
    session.setAttribute("testSessionScope", "向域对象中共享的数据");
    return "index";
}

//8.向application域对象共享数据
@RequestMapping("/testApplicationScopeByServletApi")
public String testApplicationScopeByServletApi(HttpSession session) {
    ServletContext servletContext = session.getServletContext();
    servletContext.setAttribute("testApplicationScope", "向域对象中共享的数据");
    return "index";
}

5、视图

转发视图 InternalResourceView
重定向视图 RedirectView
@Controller
@RequestMapping("/test")
public class ViewController {
    
    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    //    转发视图,游览器发送了一次请求  请求地址不变
    @RequestMapping("testForward")
    public String testForward() {
        return "forward:/test/index";
    }

    //    重定向视图,游览器发送了两次请求  请求地址改变
    @RequestMapping("testForward")
    public String testRedirect() {
        return "redirect:/test/index";
    }
} 
视图控制器 InternalResourceViewResolver
路径只是为了跳转页面 mvc:view-controller
<!--    
当访问路径仅仅只是为了跳转页面,可以通过配置 mvc:view-controller
但是配置此标签后,其他控制器将失效,因此还需要配置 <mvc:annotation-driven/>
-->
<mvc:view-controller path="/" view-name="index"/>
<mvc:annotation-driven/>

6、HttpMessageConverter 报文信息转换器

HttpMessageConverter :将请求报文转换为java对象,或 将java对象转换为响应报文

HttpMessageConverter提供两个注解和两个类型:

@RequestBody,@ResponseBody,RequestEntity,ResponseEntity,@RestController

@RequestBody

将请求报文(请求体)转换为java对象

//    @RequestBody,
@RequestMapping("form")
public String request(@RequestBody String formData) {
    //  表单的数据以JSON字符串 的形式赋值给formData
    System.out.println(formData);
    return "success";
}
@ResponseBody

将java对象转换为响应报文(响应体)

//    @ResponseBody
//此处响应的"success" 不在作为视图进行解析,而是直接作为响应体返回给客户端
//如果响应的数据是一个java对象类型的  则需要导入json依赖,则返回的java对象自动转换为json对象
@ResponseBody
@RequestMapping("response")
public String response(){
    return "success";
}
RequestEntity

可以接受整个请求报文,包括请求头和请求体

//    RequestEntity,
@RequestMapping("request2")
public String request2(RequestEntity<String> request) {
    // 获取请求头信息
    HttpHeaders headers = request.getHeaders();
    // 获取请求体信息
    String body = request.getBody();

    return "success";
}
ResponseEntity

ResponseEntity用于控制方法的返回值类型,该控制器方法的返回值就是响应到游览器的响应报文

(也就是自定义响应报文)

@RestController

@RestController 替代 @Controller

表示当前控制器所有方法返回的结果 都是响应体,不是视图

7、文件下载、上传

文件下载

通过ResponseEntity 进行文件下载

@RequestMapping("/testDown")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取服务器中文件的真实路径
    String realPath = servletContext.getRealPath("/static/img/1.jpg");
    //创建输入流
    InputStream is = new FileInputStream(realPath);
    //创建字节数组
    byte[] bytes = new byte[is.available()];
    //将流读到字节数组中
    is.read(bytes);
    //创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    //设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=1.jpg");
    //设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    //创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
    //关闭输入流
    is.close();
    return responseEntity;
}
文件上传

要求form表单的请求方式必须为post,并且添加属性enctype=“multipart/form-data”

SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息

上传步骤:

a>添加依赖

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

b>在SpringMVC的配置文件中添加配置:

<!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

c>控制器方法:

@RequestMapping("/testUp")
public String testUp(MultipartFile photo, HttpSession session) throws IOException {
    //获取上传的文件的文件名
    String fileName = photo.getOriginalFilename();
    //处理文件重名问题
    String hzName = fileName.substring(fileName.lastIndexOf("."));
    fileName = UUID.randomUUID().toString() + hzName;
    //获取服务器中photo目录的路径
    ServletContext servletContext = session.getServletContext();
    String photoPath = servletContext.getRealPath("photo");
    File file = new File(photoPath);
    if(!file.exists()){
        file.mkdir();
    }
    String finalPath = photoPath + File.separator + fileName;
    //实现上传功能
    photo.transferTo(new File(finalPath));
    return "success";
}

8、拦截器

1.拦截器的配置

SpringMVC中的拦截器用于拦截控制器方法的执行

SpringMVC中的拦截器需要实现HandlerInterceptor接口,重写preHandle方法

public class FirstInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 拦截后进行逻辑处理的内容  返回true 代表放行,返回false 表示拦截
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置:

<mvc:interceptors>
    <!--        指定规则进行拦截-->
    <mvc:interceptor>
        <!--  拦截所有请求但是不包括 "/test" 请求   /* 表示只有一层路径  /**表示任意多层路径-->
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/test"/>
        <!--  拦截请求后 进行处理的类-->
    	<bean id="firstInterceptor" class="com.potato.interceptor.FirstInterceptor"></bean>
    </mvc:interceptor>
</mvc:interceptors>
2.拦截器的三个抽象方法

SpringMVC中的拦截器有三个抽象方法:

preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法

postHandle:控制器方法执行之后执行postHandle()

afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation()

3.多个拦截器的执行顺序

a>若每个拦截器的preHandle()都返回true

此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:

preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行

b>若某个拦截器的preHandle()返回了false

preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行

客户端–> 过滤器 –> DispatcherServlet –> 拦截器 –>Controller

9、异常处理器

1.基于配置的异常处理
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <!--
                prop的键表示 需要进行处理的异常
                prop的值表示 若出现异常时,需要跳转到的视图名称
             -->
            <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <!--
        exceptionAttribute属性设置一个值,将异常信息在 请求域 中进行共享,通过ex可以获取异常的信息内容
     -->
    <property name="exceptionAttribute" value="ex"/>
</bean>
2.基于注解的异常处理
@ControllerAdvice
public class ExceptionController {

    /**
     * @param ex 异常信息
     * @param model 把异常信息添加到域对象中
     * @return 异常页面
     */
    @ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
    public String exceptionMethod(Exception ex, Model model) {
        model.addAttribute(ex);
        return "error";
    }
}

10、注解配置SpringMVC

1.WebInit

创建初始化类,代替web.xml

在Servlet3.0+环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。

public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 指定spring的配置类
     *
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    /**
     * 指定SpringMVC的配置类
     *
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    /**
     * 指定DispatcherServlet的映射规则,即url-pattern
     *
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 添加过滤器
     *
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceResponseEncoding(true);
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
    }
}

2.WebConfig

代替SpringMVC的配置文件

// 1.组件扫描  2.视图解析  3.view-controller  4.default-servlet-handler
// 5.mvc注解驱动  6.文件上传解析器  7.拦截器   8.异常处理

@Configuration
// 1.组件扫描
@ComponentScan("com.potato.controller")
// 5.mvc注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {


    //2.视图解析
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/static/");
        viewResolver.setSuffix(".jsp");
        registry.viewResolver(viewResolver);
    }

    //3.view-controller
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        WebMvcConfigurer.super.addViewControllers(registry);
    }

    //4.default-servlet-handler
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
        WebMvcConfigurer.super.configureDefaultServletHandling(configurer);
    }

    //6.文件上传解析器
    public CommonsMultipartResolver multipartResolver(CommonsMultipartResolver commonsMultipartResolver) {
        commonsMultipartResolver.setDefaultEncoding("UTF-8");
        commonsMultipartResolver.setMaxUploadSize(419430400);
        return commonsMultipartResolver;
    }

    //7.拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自己定义的拦截器
        //registry.addInterceptor(new FirstInterceptor()).addPathPatterns("/**");
        WebMvcConfigurer.super.addInterceptors(registry);
    }

    //8.异常处理  可以使用注解配置代替
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties prop = new Properties();
        prop.setProperty("java.lang.ArithmeticException", "error");
        //设置异常映射
        resolver.setExceptionMappings(prop);
        //设置共享异常信息的键
        resolver.setExceptionAttribute("ex");
        resolvers.add(resolver);
        WebMvcConfigurer.super.configureHandlerExceptionResolvers(resolvers);
    }
}

11、SpringMVC的执行流程

  1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获。

  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射:

  3. 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。

  4. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。

  5. 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】

  6. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

  7. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象。

  8. 此时将开始执行拦截器的postHandle(…)方法【逆向】。

  9. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图。

  10. 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。

  11. 将渲染结果返回给客户端。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/289030.html

(0)
上一篇 2022年9月12日
下一篇 2022年9月12日

相关推荐

发表回复

登录后才能评论