• 【SpringMVC】JSON数据返回及异常处理(相信我看完就懂的差不多了)


    目录

    一、JSON数据返回

    1、导入依赖

    2、配置文件

    3、Jackson

    3.1、介绍

    3.2、特点

    3.3、常用注解

    4、@ResponseBody注解使用

    4.1、Controller层

    4.2、biz层

    4.3、sql.xml

    4.4、页面

    4.5、测试

    4.6、注意@ResponseBody

    二、异常处理

    1、为什么要使用全局异常处理

    2、异常处理思路

    3、SpringMVC异常分类

    4、异常案例

    4.1、异常处理方法一

    4.2、异常处理方法二

    4.3、异常处理方法三


    一、JSON数据返回

    1、导入依赖

    SpringMVC里的pom.xml文件里面添加jackson的依赖

    1. <dependency>
    2. <groupId>com.fasterxml.jackson.coregroupId>
    3. <artifactId>jackson-databindartifactId>
    4. <version>2.9.3version>
    5. dependency>
    6. <dependency>
    7. <groupId>com.fasterxml.jackson.coregroupId>
    8. <artifactId>jackson-coreartifactId>
    9. <version>2.9.3version>
    10. dependency>
    11. <dependency>
    12. <groupId>com.fasterxml.jackson.coregroupId>
    13. <artifactId>jackson-annotationsartifactId>
    14. <version>2.9.3version>
    15. dependency>

    2、配置文件

    在自己配置的Spring-mvc.xml文件里面添加配置

    1. <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    2. <property name="messageConverters">
    3. <list>
    4. <ref bean="mappingJackson2HttpMessageConverter"/>
    5. list>
    6. property>
    7. bean>
    8. <bean id="mappingJackson2HttpMessageConverter"
    9. class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
    10. <property name="supportedMediaTypes">
    11. <list>
    12. <value>text/html;charset=UTF-8value>
    13. <value>text/json;charset=UTF-8value>
    14. <value>application/json;charset=UTF-8value>
    15. list>
    16. property>
    17. bean>

    3、Jackson

    3.1、介绍

    Jackson是一个用于Java对象和JSON数据之间相互转换的开源库。它提供了一组强大的API,可以将Java对象序列化为JSON格式的字符串,也可以将JSON字符串反序列化为Java对象

    Jackson库由三个核心模块组成:

    1. Jackson Databind:用于将Java对象与JSON数据进行绑定(序列化和反序列化)。它提供了一系列注解,如@JsonProperty@JsonFormat等,用于指定对象属性与JSON字段之间的映射关系,以及日期、时间等特殊类型的处理方式。Databind模块也提供了一些高级特性,如多态类型处理、双向引用处理等。

    2. Jackson Core:提供了JSON处理的核心功能,如JsonParser(用于解析JSON字符串)、JsonGenerator(用于生成JSON字符串)等。Core模块还提供了一些基本的数据类型和集合类型的序列化和反序列化支持。

    3. Jackson Annotation:提供了一些注解,用于在Java类中指定JSON字段的名称、顺序、默认值等属性。这些注解在Databind模块中使用。

    Jackson是一个简单基于Java应用库,Jackson可以轻松的将Java对象转换成json对象和xml文档,同样也可以将json、xml转换成Java对象。Jackson所依赖的jar包较少,简单易用并且性能也要相对高些,并且Jackson社区相对比较活跃,更新速度也比较快。

    3.2、特点

    • 容易使用,提供了高层次外观,简化常用的用例。
    • 无需创建映射,API提供了默认的映射大部分对象序列化。
    • 性能高,快速,低内存占用
    • 创建干净的json
    • 不依赖其他库
    • 代码开源

    3.3、常用注解

    Jackson库提供了一些常用的注解,用于在Java类中指定与JSON字段相关的属性和行为。

    常用注解
    注解说明
    @JsonIgnore
     
    作用在字段或方法上,用来完全忽略被注解的字段和方法对应的属性
     
    @JsonProperty
     
    作用在字段或方法上,用来对属性的序列化/反序列化,可以用来避免遗漏属性,同时提供对属性名称重命名
     
    @JsonIgnoreProperties
     
    作用在类上,用来说明有些属性在序列化/反序列化时需要忽略掉
     
    @JsonUnwrapped
     
    作用在属性字段或方法上,用来将子JSON对象的属性添加到封闭的JSON对象
     
    @JsonFormat
     
    用于指定日期、时间等特殊类型的格式化方式。可以通过该注解的pattern属性指定日期格式。例如:
    @JsonFormat(pattern="yyyy-MM-dd") private Date birthDate;

    1. @JsonInclude:用于控制序列化时是否包含某个属性。可以通过该注解的value属性指定包含属性的条件。例如:

      @JsonInclude(JsonInclude.Include.NON_NULL) private String address;

    2. @JsonAlias:用于指定多个JSON字段与同一个Java属性的映射关系。可以通过该注解的value属性指定多个JSON字段的名称。例如:

      @JsonAlias({"firstName", "first_name"}) private String name;

    3. @JsonGetter和@JsonSetter:用于指定自定义的getter和setter方法与JSON字段之间的映射关系。例如:

      @JsonGetter("name") public String getUserName() { return userName; }

    4. @JsonAnyGetter和@JsonAnySetter:用于处理不确定的属性(即不在Java类中定义的属性)。可以通过这些注解来动态地将这些属性转换为Map或其他形式。例如:

      @JsonAnyGetter public Map getAdditionalProperties() { return additionalProperties; }

    4、@ResponseBody注解使用

    @ResponseBody注解是Spring框架中常用的注解之一,它通常用于控制器方法或Restful API方法上,用于指示方法返回的结果直接写入HTTP响应体中,而不是通过视图解析器进行解析。它可以将方法返回的对象转换为指定的格式,如JSON、XML或其他格式,并将其作为HTTP响应体返回给客户端。

    作用:将Controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据。

     注意:在使用此注解之后不会再走视图解析器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。

    @ResponseBody注解的方法,可以返回任何类型的对象作为响应体,包括普通对象、集合、字符串等。Spring会自动根据请求头中的"Accept"字段,选择合适的HttpMessageConverter进行转换。例如,如果请求头中指定了"Accept: http://localhost:8080/json",则返回的对象会被转换为JSON格式的字符串。

    4.1、Controller层

    1. package com.tgq.web;
    2. import com.tgq.biz.MyStudentBiz;
    3. import com.tgq.model.MyStudent;
    4. import com.tgq.utils.PageBean;
    5. import org.springframework.beans.factory.annotation.Autowired;
    6. import org.springframework.stereotype.Controller;
    7. import org.springframework.web.bind.annotation.RequestMapping;
    8. import org.springframework.web.bind.annotation.ResponseBody;
    9. import javax.servlet.http.HttpServletRequest;
    10. import java.util.HashMap;
    11. import java.util.List;
    12. import java.util.Map;
    13. /**
    14. * @软件包名 com.tgq.web
    15. * @用户 tgq
    16. * @create 2023-09-13 下午2:27
    17. * @注释说明:
    18. */
    19. @Controller
    20. //@ResponseBody
    21. //@RestController
    22. @RequestMapping("/sc/json")
    23. public class JsonController {
    24. @Autowired
    25. private MyStudentBiz myStudentBiz;
    26. @RequestMapping("/jsp")
    27. public String updateByPrimaryKeySelective() {
    28. return "json/list";
    29. }
    30. /**
    31. * 返回List
    32. *
    33. * @param req
    34. * @param MyStudent
    35. * @return
    36. */
    37. @ResponseBody
    38. @RequestMapping("/list")
    39. public List list(HttpServletRequest req, MyStudent MyStudent) {
    40. PageBean pageBean = new PageBean();
    41. pageBean.setRequest(req);
    42. List list = this.myStudentBiz.selectPager(MyStudent, pageBean);
    43. return list;
    44. }
    45. /**
    46. * 返回T
    47. *
    48. * @param req
    49. * @param MyStudent
    50. * @return
    51. */
    52. @ResponseBody
    53. @RequestMapping("/load")
    54. public MyStudent load(HttpServletRequest req, MyStudent MyStudent) {
    55. if (MyStudent.getSname() != null) {
    56. List list = this.myStudentBiz.selectPager(MyStudent, null);
    57. return list.get(0);
    58. }
    59. return null;
    60. }
    61. /**
    62. * 返回List
    63. *
    64. * @param req
    65. * @param MyStudent
    66. * @return
    67. */
    68. @ResponseBody
    69. @RequestMapping("/mapList")
    70. public List mapList(HttpServletRequest req, MyStudent MyStudent) {
    71. PageBean pageBean = new PageBean();
    72. pageBean.setRequest(req);
    73. List list = this.myStudentBiz.mapSelectPager(MyStudent, pageBean);
    74. return list;
    75. }
    76. /**
    77. * 返回Map
    78. *
    79. * @param req
    80. * @param MyStudent
    81. * @return
    82. */
    83. @ResponseBody
    84. @RequestMapping("/mapLoad")
    85. public Map mapLoad(HttpServletRequest req, MyStudent MyStudent) {
    86. if (MyStudent.getSname() != null) {
    87. List list = this.myStudentBiz.mapSelectPager(MyStudent, null);
    88. return list.get(0);
    89. }
    90. return null;
    91. }
    92. @ResponseBody
    93. @RequestMapping("/all")
    94. public Map all(HttpServletRequest req, MyStudent MyStudent) {
    95. PageBean pageBean = new PageBean();
    96. pageBean.setRequest(req);
    97. List list = this.myStudentBiz.selectPager(MyStudent, pageBean);
    98. Map map = new HashMap();
    99. map.put("list", list);
    100. map.put("pageBean", pageBean);
    101. return map;
    102. }
    103. @ResponseBody//注释 跳的就是页面,就不是显示的字符串
    104. @RequestMapping("/jsonStr")
    105. public String jsonStr(HttpServletRequest req, MyStudent MyStudent) {
    106. return "sc-edit";
    107. }
    108. }

    4.2、biz层

    1. @Override
    2. public List mapSelectPager(MyStudent myStudent, PageBean pageBean) {
    3. return myStudentMapper.mapSelectPager(myStudent);
    4. }

    4.3、sql.xml

    1. <select id="mapSelectPager" resultType="java.util.Map" parameterType="com.tgq.model.MyStudent">
    2. select *
    3. from t_mysql_student
    4. <where>
    5. <if test="sname != null"/>
    6. and sname like concat('%',#{sname},'%')
    7. where>
    8. select>

    4.4、页面

    1. <%--
    2. Created by IntelliJ IDEA.
    3. User: tgq
    4. Date: 13/9/2023
    5. Time: 下午2:43
    6. To change this template use File | Settings | File Templates.
    7. --%>
    8. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    9. <html>
    10. <head>
    11. <title>Titletitle>
    12. head>
    13. <body>
    14. <h3>JSONh3>
    15. <a href="${pageContext.request.contextPath}/sc/json/list?sname=">返回List<T>a>
    16. <hr>
    17. <a href="${pageContext.request.contextPath}/sc/json/mapList?sname=">返回List<Map>a>
    18. <hr>
    19. <a href="${pageContext.request.contextPath}/sc/json/load?sname=三">返回Ta>
    20. <hr>
    21. <a href="${pageContext.request.contextPath}/sc/json/mapLoad?sname=三">返回Mapa>
    22. <hr>
    23. <a href="${pageContext.request.contextPath}/sc/json/all?sname=">返回混合a>
    24. <hr>
    25. <a href="${pageContext.request.contextPath}/sc/json/jsonStr">返回JSON字符串a>
    26. <hr>
    27. body>
    28. html>

    4.5、测试

    http://localhost:8080/sc/json/jsp

    4.6、注意@ResponseBody

    如果在类名上面加上@ResponseBody注解,里面所有的方法返回的都会是字符串

    @RestController:可以代替@Controller@ResponseBody

    二、异常处理

    1、为什么要使用全局异常处理

    全局异常处理是一种在应用程序中集中处理异常的方式。它的主要目的是提供一种统一的机制来处理应用程序中抛出的异常,而不是在每个可能抛出异常的地方都进行单独的处理。

    使用全局异常处理的好处:

    1. 代码简洁:通过全局异常处理,我们可以将异常处理逻辑从业务代码中分离出来,使业务代码更加简洁和清晰。业务代码只需要关注核心业务逻辑,而将异常处理交给专门的异常处理器。

    2. 统一异常处理:全局异常处理提供了一种统一的机制来处理应用程序中的异常。无论是在控制器中处理请求时抛出的异常,还是在服务层、持久层或其他地方抛出的异常,都可以在全局异常处理器中进行处理。这样可以确保应用程序中的所有异常都能得到统一的处理,提高开发效率和代码质量。

    3. 异常信息友好化:通过全局异常处理,我们可以自定义异常信息的格式和内容,使其更加友好和易于理解。我们可以根据具体的业务需求,将异常信息转化为适合用户展示的形式,或者记录到日志中以便问题排查。

    4. 统一异常返回:在全局异常处理中,我们可以统一定义异常返回的格式和内容。例如,可以定义一个通用的错误响应对象,用于封装异常信息,并统一返回给客户端。这样可以避免在每个控制器方法中都手动处理异常并返回错误信息。

    5. 安全性:通过全局异常处理,我们可以捕获并处理未被处理的异常,防止异常信息暴露给客户端,从而提高应用程序的安全性。

    全局异常处理提供了一种集中处理应用程序中异常的方式,简化了代码逻辑,提高了代码的可读性和可维护性。它使我们能够更好地处理异常、优化用户体验,并提高应用程序的稳定性和安全性。

    2、异常处理思路

    第一种思路:

    系统的dao、service、controller出现异常都通过throws Exception向上抛出,最后由springmvc前端控制器交由异常处理器进行异常处理。springmvc提供全局异常处理器(一个系统只有一个异常处理器)进行统一异常处理。

    也可以遵循第二种思路:

    1. 捕获异常:在代码中使用try-catch语句块捕获可能抛出的异常。在try块中放置可能抛出异常的代码,而在catch块中处理捕获的异常。可以根据不同的异常类型,使用多个catch块分别处理不同类型的异常。

    2. 日志记录:在异常处理过程中,应该记录异常信息,以便后续的问题排查和调试。可以使用日志框架(如Log4j、Slf4j等)记录异常信息,包括异常类型、异常消息、堆栈跟踪等。通过日志记录,可以及时发现和解决潜在的问题。

    3. 友好提示:根据具体的业务需求,可以将异常信息转化为友好的提示信息,以便用户理解和处理。可以通过自定义异常类,设置异常的错误码和错误信息,然后根据错误码返回相应的提示信息给用户。

    4. 异常返回:在Web应用中,可以使用全局异常处理器将异常信息封装为统一的错误响应,并返回给客户端。这样可以提供统一的异常处理逻辑,避免在每个控制器方法中都手动处理异常。

    5. 异常链传递:在捕获异常后,可以通过抛出新的异常或将原始异常作为参数传递给上层调用者,以便上层调用者能够处理该异常。通过异常链的方式,可以将异常信息传递到合适的地方进行处理。

    6. 异常处理策略:对于可恢复的异常,可以采取相应的处理策略,尝试恢复正常的程序执行。例如,可以重新连接数据库、重新发送请求等。对于不可恢复的异常,可以通知相关人员或进行紧急处理。

    7. 异常监控和统计:可以使用监控工具或日志分析工具对异常进行监控和统计,以便及时发现和解决异常问题。通过收集和分析异常信息,可以帮助优化系统性能和提高用户体验。

    处理异常的思路是捕获异常、记录异常、友好提示、异常返回,根据具体情况采取合适的处理策略,并进行异常监控和统计。通过良好的异常处理,可以提高应用程序的稳定性和可维护性,优化用户体验,并及时发现和解决潜在的问题。

    3、SpringMVC异常分类

    • 使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; 
    • 实现Spring的异常处理接口HandlerExceptionResolver自定义自己的异常处理器;
    • 使用@ControllerAdvice + @ExceptionHandler

    1. 控制器异常:控制器异常是在处理客户端请求时发生的异常。这些异常通常由控制器方法抛出,可以包括验证错误、请求参数错误等。可以使用@ExceptionHandler注解来处理控制器异常,将异常处理逻辑集中在一个地方,统一返回错误信息给客户端。

    2. 服务层异常:服务层异常通常由业务逻辑引起,例如数据查询失败、业务规则校验失败等。服务层异常应该在服务层中进行捕获和处理,可以使用try-catch语句块或自定义的异常处理器来处理服务层异常。

    3. 数据访问异常:数据访问异常主要由数据库操作引起,例如数据库连接失败、SQL语句执行错误等。在Spring MVC中,可以使用Spring的数据访问框架(如Spring Data JPA、MyBatis等)来处理数据访问异常,框架会自动将底层的异常转化为Spring的数据访问异常(如DataAccessException)。

    4. 全局异常:全局异常是指在应用程序中没有被捕获和处理的异常,或者是无法预知的异常。可以使用全局异常处理器(@ControllerAdvice注解)来处理全局异常,将异常处理逻辑集中在一个地方,以便统一处理和返回错误信息。

    5. 运行时异常:运行时异常(RuntimeException)是一种不需要显式捕获和处理的异常。这些异常通常是由程序错误或逻辑错误引起的,例如空指针异常、数组越界异常等。在Spring MVC中,运行时异常会被Servlet容器捕获并返回默认的错误页面(如500 Internal Server Error页面)。

    对于不同类型的异常,可以根据具体需求选择不同的处理方式。有些异常可以通过捕获和处理来恢复正常的程序执行,而有些异常可能需要通知相关人员或进行紧急处理。通过合理分类和处理异常,可以提高应用程序的稳定性和可维护性。

    4、异常案例

    4.1、异常处理方法一

    SpringMVC中自带了一个异常处理器叫SimpleMappingExceptionResolver,该处理器实现了HandlerExceptionResolver 接口,全局异常处理器都需要实现该接口。

    spring-mvc.xml

    1. <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    2. <property name="defaultErrorView" value="error"/>
    3. <property name="exceptionAttribute" value="ex"/>
    4. <property name="exceptionMappings">
    5. <props>
    6. <prop key="java.lang.RuntimeException">errorprop>
    7. props>
    8. property>
    9. bean>

    页面跳转由SpringMVC来接管了,所以此处的定义默认的异常处理页面都应该配置成逻辑视图名。

    1. @RequestMapping("/error")
    2. public String error(MyStudent MyStudent) {
    3. MyStudent = null;
    4. MyStudent.getSname();
    5. return "error";
    6. }

    写一个异常的界面

    1. <%--
    2. Created by IntelliJ IDEA.
    3. User: tgq
    4. Date: 13/9/2023
    5. Time: 下午6:10
    6. To change this template use File | Settings | File Templates.
    7. --%>
    8. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    9. <html>
    10. <head>
    11. <title>errortitle>
    12. head>
    13. <body>
    14. 错误信息....
    15. ${ex}
    16. body>
    17. html>

    测试一下效果

    http://localhost:8080/sc/json/error

    4.2、异常处理方法二

    创建两个包里面写两个类

    GlobalException 

    1. package com.tgq.exception;
    2. public class GlobalException extends RuntimeException {
    3. public GlobalException() {
    4. }
    5. public GlobalException(String message) {
    6. super(message);
    7. }
    8. public GlobalException(String message, Throwable cause) {
    9. super(message, cause);
    10. }
    11. public GlobalException(Throwable cause) {
    12. super(cause);
    13. }
    14. public GlobalException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
    15. super(message, cause, enableSuppression, writableStackTrace);
    16. }
    17. }


     

    GlobalExceptionHandler 

    1. package com.tgq.component;
    2. import com.tgq.exception.GlobalException;
    3. import org.springframework.stereotype.Component;
    4. import org.springframework.web.servlet.HandlerExceptionResolver;
    5. import org.springframework.web.servlet.ModelAndView;
    6. import javax.servlet.http.HttpServletRequest;
    7. import javax.servlet.http.HttpServletResponse;
    8. @Component
    9. public class GlobalExceptionHandler implements HandlerExceptionResolver {
    10. @Override
    11. public ModelAndView resolveException(HttpServletRequest httpServletRequest,
    12. HttpServletResponse httpServletResponse,
    13. Object o, Exception e) {
    14. ModelAndView mv = new ModelAndView();
    15. mv.setViewName("error");
    16. if (e instanceof GlobalException) {
    17. GlobalException globalException = (GlobalException) e;
    18. mv.addObject("ex", globalException.getMessage());
    19. mv.addObject("msg", "全局异常....");
    20. } else if (e instanceof RuntimeException) {
    21. RuntimeException runtimeException = (RuntimeException) e;
    22. mv.addObject("ex", runtimeException.getMessage());
    23. mv.addObject("msg", "运行时异常....");
    24. } else if (e instanceof RuntimeException) {
    25. RuntimeException runtimeException = (RuntimeException) e;
    26. mv.addObject("ex", e.getMessage());
    27. mv.addObject("msg", "其他异常....");
    28. }
    29. return mv;
    30. }
    31. }

    后面也可自定义异常

    写一个方法测试

    1. @RequestMapping("/error02")
    2. public String error2(HttpServletRequest req) {
    3. if (true)
    4. throw new GlobalException("老异常了");
    5. return "error";
    6. }

    页面代码

    1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    2. <html>
    3. <head>
    4. <title>errortitle>
    5. head>
    6. <body>
    7. 错误信息....
    8. ${msg}
    9. body>
    10. html>

    http://localhost:8080/sc/json/error02

    4.3、异常处理方法三

    可以把component包里面的GlobalExceptionHandler 换成GlobalExceptionResolver 
    GlobalExceptionResolver 
    :里面返回的json格式和跳转页面

    1. package com.tgq.component;
    2. import com.tgq.exception.GlobalException;
    3. import org.springframework.web.bind.annotation.ControllerAdvice;
    4. import org.springframework.web.bind.annotation.ExceptionHandler;
    5. import org.springframework.web.bind.annotation.ResponseBody;
    6. import java.util.HashMap;
    7. import java.util.Map;
    8. @ControllerAdvice
    9. public class GlobalExceptionResolver {
    10. // 跳转错误页面
    11. // @ExceptionHandler
    12. // public ModelAndView handler(Exception e){
    13. // ModelAndView mv = new ModelAndView();
    14. // mv.setViewName("error");
    15. // if (e instanceof GlobalException){
    16. // GlobalException globalException = (GlobalException) e;
    17. // mv.addObject("ex",globalException.getMessage());
    18. // mv.addObject("msg","全局异常....");
    19. // }else if (e instanceof RuntimeException){
    20. // RuntimeException runtimeException = (RuntimeException) e;
    21. // mv.addObject("ex",runtimeException.getMessage());
    22. // mv.addObject("msg","运行时异常....");
    23. // }
    24. // return mv;
    25. // }
    26. // 返回错误json数据
    27. @ResponseBody
    28. @ExceptionHandler
    29. public Map handler(Exception e) {
    30. Map map = new HashMap();
    31. if (e instanceof GlobalException) {
    32. GlobalException globalException = (GlobalException) e;
    33. map.put("ex", globalException.getMessage());
    34. map.put("msg", "全局异常....");
    35. } else if (e instanceof RuntimeException) {
    36. RuntimeException runtimeException = (RuntimeException) e;
    37. map.put("ex", runtimeException.getMessage());
    38. map.put("msg", "运行时异常....");
    39. } else {
    40. map.put("ex", e.getMessage());
    41. map.put("msg", "其它异常....");
    42. }
    43. return map;
    44. }
    45. }

    测试结果

    json格式

    页面

  • 相关阅读:
    基于Java+SpringCloud+Mybaties-plus+Vue+elememt 智能停车场管理系统 的设计与实现
    Python:实现first come first served先到先得算法(附完整源码)
    Kubernetes学习笔记-kubernetes API服务器的安全防护(1)通过基于角色的权限控制加强集群安全20220814
    什么是DeFi流动性资金池
    推荐一个基于Python开源的文档系统
    Java基础—循环栅栏CyclicBarrier
    postgrest API CURD数据库
    Java 异步编程 (5 种异步实现方式详解)
    关于写一个read_file的很多个为什么&如何用
    什么是HTML?
  • 原文地址:https://blog.csdn.net/weixin_74383330/article/details/132852735