Spring MVC使用篇(七)—— 参数绑定

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_36378917/article/details/83660999

1、参数绑定综述

  当用户在页面触发某种请求时,一般会将一些参数(key/value)带到后台。在Spring MVC中可以通过参数绑定,将客户端请求的key/value数据绑定到Controller处理器方法的形参上,显然,这是很关键的一个问题。

  当用户发送一个请求时,根据Spring MVC的请求处理流程,前端控制器会请求处理器映射器HandlerMapping返回一个处理器(或处理器链),然后请求处理器适配器HandlerAdapter执行相应的Handler处理器。此时,处理器适配器HandlerAdapter会调用Spring MVC提供的参数绑定组件将请求的key/value数据绑定到Controller处理器方法对应的形参上。

  关于Spring MVC的参数绑定组件,早期版本中使用PropertyEditor,其只能将字符串转换为Java对象,而后期版本中使用Converter转换器,它可以进行任意类型的转换。Spring MVC提供了很多的Converter转换器,但在特殊情况下需要自定义Converter转换器(如日期数据绑定)。

  Spring MVC中有一些默认支持的类型,这些类型可以直接在Controller类的方法中定义,在参数绑定的过程中遇到该种类型就直接进行绑定。其默认支持的类型有以下几种:HttpServletRequest、HttpServletResponse、HttpSession及Model/ModelMap。

  HttpServletRequest可以通过request对象获取请求信息。

  HttpServletResponse可以通过response对象处理响应信息。

  HttpSession可以通过session对象得到session中存放的对象。

  Model是一个接口,ModelMap是一个接口实现,它的作用就是讲model数据填充到request域。

2 、简单类型参数绑定

  在Spring MVC中还可以自定义简单类型,这些类型也是直接在Controller类的方法中定义,在处理key/value信息时,就会以key名寻找Controller类的方法中具有相同名称的形参并进行绑定。

  例如下面的例子,根据水果商品的id来获取水果的详细信息,具体Controller代码如下:

package com.ccff.controller;

import com.ccff.model.Fruits;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

//基于注解的Handler类
//使用 @Controller来标识它是一个控制器
@Controller
@RequestMapping("/query")
public class FruitsController {
    private FruitsService fruitsService = new FruitsService();
    //根据水果id查询水果信息
    @RequestMapping(value = "/queryFruitByID.action", method = RequestMethod.GET)
    public String queryFruitByID(Model model, int id){
        //模拟service获取水果商品列表
        List<Fruits> fruitsList = fruitsService.queryFruitsList();
        List<Fruits> resultFruit = new ArrayList<>();
        for (Fruits fruits : fruitsList){
            if (fruits.getId() == id)
                resultFruit.add(fruits);
        }
        //将结果放到model中传到显示页面中
        model.addAttribute("resultFruit",resultFruit);
        return "fruits/resultFruitList";
    }         
}

class FruitsService{
    public List<Fruits> queryFruitsList(){
        List<Fruits> fruitsList = new ArrayList<Fruits>();

        Fruits apple = new Fruits();
        apple.setId(1);
        apple.setName("红富士苹果");
        apple.setPrice(2.3);
        apple.setProducing_area("山东");

        Fruits banana = new Fruits();
        banana.setId(2);
        banana.setName("香蕉");
        banana.setPrice(1.5);
        banana.setProducing_area("上海");

        fruitsList.add(apple);
        fruitsList.add(banana);
        return fruitsList;
    }
}

  可以在执行queryFruitByID请求时,为其指定一个参数id。由于通过RequestMapping的method属性指定了请求类型为GET类型,所以把项目部署在Tomcat上后,可以使用下面的URL请求:http://localhost:8080/demo/query/queryFruitByID.action?id=1就可以获得id为1的水果商品的详细信息,结果如图所示:
在这里插入图片描述
  当然,如果参数名字不为“id”,绑定就不会成功,不过可以通过使用注解的方式为请求参数指定别名。注解@RequestParam可以对自定义简单类型的参数进行绑定,即如果使用@RequestParam,就无须设置Controller方法的形参名称与request传入的参数名称一致。而不使用@RequestParam时,就需要Controller方法的形参名称与request传入的参数名称一致,这样才能够绑定成功。

  假设执行queryFruitByID请求时,传入的id属性名称为fruit_id,而Java代码使用的是驼峰命名,那么可以通过@RequestParam注解来指定绑定名称,而在形参中继续使用驼峰命名,Controller具体代码如下:

package com.ccff.controller;

import com.ccff.model.Fruits;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

//基于注解的Handler类
//使用 @Controller来标识它是一个控制器
@Controller
@RequestMapping("/query")
public class FruitsController {
    private FruitsService fruitsService = new FruitsService();
    //根据水果id查询水果信息
    @RequestMapping(value = "/queryFruitByID.action", method = RequestMethod.GET)
    public String queryFruitByID(Model model, @RequestParam(value = "fruit_id") int fruitId){
        //模拟service获取水果商品列表
        List<Fruits> fruitsList = fruitsService.queryFruitsList();
        List<Fruits> resultFruit = new ArrayList<>();
        for (Fruits fruits : fruitsList){
            if (fruits.getId() == fruitId)
                resultFruit.add(fruits);
        }
        //将结果放到model中传到显示页面中
        model.addAttribute("resultFruit",resultFruit);
        return "fruits/resultFruitList";
    }         
}

class FruitsService{
    public List<Fruits> queryFruitsList(){
        List<Fruits> fruitsList = new ArrayList<Fruits>();

        Fruits apple = new Fruits();
        apple.setId(1);
        apple.setName("红富士苹果");
        apple.setPrice(2.3);
        apple.setProducing_area("山东");

        Fruits banana = new Fruits();
        banana.setId(2);
        banana.setName("香蕉");
        banana.setPrice(1.5);
        banana.setProducing_area("上海");

        fruitsList.add(apple);
        fruitsList.add(banana);
        return fruitsList;
    }
}

  再次将项目部署到Tomcat上后,在浏览器输入请求URL:http://localhost:8080/demo/query/queryFruitByID.action?fruit_id=1 ,可得如下结果,证明正确。
在这里插入图片描述
  当Controller方法有多个形参时,如果请求中不包含其中的某个形参,此时是不会报错的,所以使用该参数时要进行空校验。如果要求绑定的参数一定不能为空,可以使用@RequestParam注解中的required属性来指定该形参是否必须传入,required属性为“true”指定参数必须传入。

  修改Controller中的代码如下:

package com.ccff.controller;

import com.ccff.model.Fruits;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

//基于注解的Handler类
//使用 @Controller来标识它是一个控制器
@Controller
@RequestMapping("/query")
public class FruitsController {
    private FruitsService fruitsService = new FruitsService();
    //根据水果id查询水果信息
    @RequestMapping(value = "/queryFruitByID.action", method = RequestMethod.GET)
    public String queryFruitByID(Model model, @RequestParam(value = "fruit_id", required = true) int fruitId){
        //模拟service获取水果商品列表
        List<Fruits> fruitsList = fruitsService.queryFruitsList();
        List<Fruits> resultFruit = new ArrayList<>();
        for (Fruits fruits : fruitsList){
            if (fruits.getId() == fruitId)
                resultFruit.add(fruits);
        }
        //将结果放到model中传到显示页面中
        model.addAttribute("resultFruit",resultFruit);
        return "fruits/resultFruitList";
    }         
}

class FruitsService{
    public List<Fruits> queryFruitsList(){
        List<Fruits> fruitsList = new ArrayList<Fruits>();

        Fruits apple = new Fruits();
        apple.setId(1);
        apple.setName("红富士苹果");
        apple.setPrice(2.3);
        apple.setProducing_area("山东");

        Fruits banana = new Fruits();
        banana.setId(2);
        banana.setName("香蕉");
        banana.setPrice(1.5);
        banana.setProducing_area("上海");

        fruitsList.add(apple);
        fruitsList.add(banana);
        return fruitsList;
    }
}

  在上面的例子,如果请求中没有包含fruit_id这个参数,直接请求URL:http://localhost:8080/demo/query/queryFruitByID.action ,则会报下图所示错误:
在这里插入图片描述
  在Controller方法的形参中,如果有一些参数可能为空,但是又期望它们为空时有一个默认值,此时可以使用@RequestParam注解中的defaultValue属性来指定某些参数的默认值。

  如将之前的Controller中queryFruitByID方法中的fruitId参数设置为空时的默认值为1,具体代码如下:

package com.ccff.controller;

import com.ccff.model.Fruits;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

//基于注解的Handler类
//使用 @Controller来标识它是一个控制器
@Controller
@RequestMapping("/query")
public class FruitsController {
    private FruitsService fruitsService = new FruitsService();
    //根据水果id查询水果信息
    @RequestMapping(value = "/queryFruitByID.action", method = RequestMethod.GET)
    public String queryFruitByID(Model model, @RequestParam(value = "fruit_id", defaultValue = "1") int fruitId){
        //模拟service获取水果商品列表
        List<Fruits> fruitsList = fruitsService.queryFruitsList();
        List<Fruits> resultFruit = new ArrayList<>();
        for (Fruits fruits : fruitsList){
            if (fruits.getId() == fruitId)
                resultFruit.add(fruits);
        }
        //将结果放到model中传到显示页面中
        model.addAttribute("resultFruit",resultFruit);
        return "fruits/resultFruitList";
    }         
}

class FruitsService{
    public List<Fruits> queryFruitsList(){
        List<Fruits> fruitsList = new ArrayList<Fruits>();

        Fruits apple = new Fruits();
        apple.setId(1);
        apple.setName("红富士苹果");
        apple.setPrice(2.3);
        apple.setProducing_area("山东");

        Fruits banana = new Fruits();
        banana.setId(2);
        banana.setName("香蕉");
        banana.setPrice(1.5);
        banana.setProducing_area("上海");

        fruitsList.add(apple);
        fruitsList.add(banana);
        return fruitsList;
    }
}

  在上面的例子中,如果请求中没哟fruit_id参数,或者id参数值为空,此时处理器适配器会使用参数绑定组件将fruit_id的默认值defaultValue(为1)取出赋给形参fruitId。将项目部署到Tomcat上后,在浏览器直接请求如下URL:http://localhost:8080/demo/query/queryFruitByID.actionhttp://localhost:8080/demo/query/queryFruitByID.action?fruit_id= ,均可以得到id值为1的水果商品详细信息,结果如下图所示:
在这里插入图片描述
在这里插入图片描述

3、 包装类型参数绑定

3.1 单一包装类型参数绑定

  在开发过程中,Web端可以接受并绑定绑定包装类型的数据,在Service业务层稍微处理之后,交给DAO数据处理层处理。当Web端是由Spring MVC前端控制框架来控制时,Controller方法除了可以映射简单类型的参数外,还可以映射前台页面中包含的包装类型参数。

  为了更好的理解Spring MVC处理包装类型的方式,我们接下来实现一个通过水果名和水果产地来搜索水果商品信息这样的一个功能。首先,在web/WEB-INF/jsp/fruits路径下添加名为“findFruits.jsp”的JSP页面,其中有名称、产地的搜索框和搜索按钮,搜索结果在下面以table列表形式显示,具体代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>   
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
<html>  
<head>  
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
  <title>水果搜索</title>  
</head>  
<body>
  <h3>水果搜索</h3>

  <form action="queryFruitsByCondition.action" method="post">
      名称:<input type="text" name="name" />&nbsp;&nbsp;
      产地:<input type="text" name="producing_area" />
      <input type="submit" value="搜索" />
  </form>

  <hr/>

  <h3>水果搜索结果</h3>

  <table width="300px;" border=1>  
    <tr>
      <td>编号</td>
      <td>名称</td>  
      <td>价格</td>    
      <td>产地</td> 
   </tr>  
   <c:if test="${findList==null}">
      <b>水果商品信息为空!</b>
   </c:if>
   <c:forEach items="${findList}" var="fruit">
     <tr>
       <td>${fruit.id }</td>
       <td>${fruit.name }</td>  
       <td>${fruit.price }</td>    
       <td>${fruit.producing_area }</td>  
     </tr>  
    </c:forEach>  
   </table>   
</body>
</html> 

  可以看到,搜索区域是包裹在form表单中的,其中要请求的action地址为要在Controller中编写的模糊搜索方法对应的URL“queryFruitsByCondition.action”,而搜索条件的input中,可以看到name指定的名称为Fruits包装类中的属性名,这种类型将会被Spring MVC的处理器适配器解析,它会创建具体的实体类,并将相关的属性值通过set方法绑定到包装类中

  在controller包中创建名为“FindController”的类,然后给该类添加代表控制器的注解@Controller,然后编写名为“queryFruitsByCondition”的方法,并指定方法参数为Fruits实体类,由于是模糊搜索,所以返回多个搜索结果,是一个list集合。方法中的逻辑就是将从前端页面传来的Fruits实体类,传递给Service的模糊查询方法,得到结果。

  Fruits模型类的具体代码如下:

package com.ccff.model;

public class Fruits {

    private int id;         //水果编号
    private String name;    //水果名
    private double price;   //水果价格
    private String producing_area;  //水果产地

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public String getProducing_area() {
        return producing_area;
    }

    public void setProducing_area(String producing_area) {
        this.producing_area = producing_area;
    }
}

  Controller类的具体代码如下:

package com.ccff.controller;

import com.ccff.model.Fruits;
import com.ccff.service.FruitsService;
import com.ccff.service.FruitsServiceImpl;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/query")
public class FindController {

    private FruitsService fruitsService = new FruitsServiceImpl();

    @RequestMapping("queryFruitsByCondition")
    public String queryFruitsByCondition(Model model, Fruits fruits){
        List<Fruits> findList = null;
        if (fruits == null || (fruits.getName() == null && fruits.getProducing_area() == null)){
            //如果fruits或查询条件为空,默认查询所有数据
            findList = fruitsService.queryFruitsList();
        }else {
            //如果fruits查询条件不为空,按条件查询
            findList = fruitsService.queryFruitsByCondition(fruits);
        }
        //将model数据传到页面
        model.addAttribute("findList",findList);
        return "fruits/findFruits";
    }

}

  在类名及queryFruitsByCondition方法上,分别使用了@RequestMapping注解,指定了请求响应,URL为“/query/queryFruitsByCondition”(action后缀在web.xml中设置)。在该方法中设置了两个参数,一个是返回视图数据的Model对象,一个是接收前端页面绑定的实体类对象(封装了form表单中的input标签中的参数)。通过形参活动Fruits实体对象,然后根据Fruits的空值情况判断调用不同的Service方法来获取不同的数据。最终返回了一个JSP视图的路径。

  另外,这里将Service从之前的测试类中抽象出来,分为接口和实现。

  Service层接口代码如下:

package com.ccff.service;

import com.ccff.model.Fruits;

import java.util.List;

public interface FruitsService {
    public List<Fruits> queryFruitsList();
    public Fruits queryFruitById(int id);
    public List<Fruits> queryFruitsByCondition(Fruits fruits);
}

  Service层接口实现类代码如下:

package com.ccff.service;

import com.ccff.model.Fruits;

import java.util.ArrayList;
import java.util.List;

public class FruitsServiceImpl implements FruitsService {
    public List<Fruits> fruitsList = null;

    /**
     * 初始化水果商品水果数据
     * @return
     */
    public List<Fruits> initFruitsData(){
        if (fruitsList == null){
            fruitsList = new ArrayList<Fruits>();

            Fruits apple = new Fruits();
            apple.setId(1);
            apple.setName("红富士苹果");
            apple.setPrice(2.3);
            apple.setProducing_area("山东");

            Fruits banana = new Fruits();
            banana.setId(2);
            banana.setName("香蕉");
            banana.setPrice(1.5);
            banana.setProducing_area("上海");

            fruitsList.add(apple);
            fruitsList.add(banana);
            return fruitsList;
        }else {
            return fruitsList;
        }
    }

    /**
     * 获取所有水果商品数据
     * @return
     */
    @Override
    public List<Fruits> queryFruitsList() {
        initFruitsData();
        return fruitsList;
    }

    /**
     * 根据给出的水果商品的id查询获取该水果商品的详细信息
     * @param id
     * @return
     */
    @Override
    public Fruits queryFruitById(int id) {
        initFruitsData();
        for (Fruits fruits : fruitsList){
            if (fruits.getId() == id)
                return fruits;
        }
        return null;
    }

    /**
     * 根据给出的搜索条件构造Fruits实体类,搜索水果商品详细信息
     * @param fruits
     * @return
     */
    @Override
    public List<Fruits> queryFruitsByCondition(Fruits fruits) {
        initFruitsData();
        String name = fruits.getName();
        String area = fruits.getProducing_area();
        List<Fruits> queryList = new ArrayList<Fruits>();
        Fruits f;
        for (int i = 0; i < fruitsList.size(); i++) {
            f = fruitsList.get(i);
            //有一项符合条件就返回
            if ((!"".equals(name) && f.getName().contains(name)) ||
                    (!"".equals(area) && f.getProducing_area().contains(area)))
                queryList.add(f);
        }
        return queryList.size()>0?queryList:null;
    }
}

  将项目发布到Tomcat上后,在浏览器输入请求URL:http://localhost:8080/demo/query/queryFruitsByCondition.action ,第一次由于搜索框中输入的内容为空,即由前台页面传递给Controller的Fruits实体类为空,因此默认显示所有水果商品的信息,如下图所示:
在这里插入图片描述
  当在搜索框中输入水果名为“苹果”时,点击搜索按钮,则结果如下图所示,说明包装类型的Fruits参数已经成功与Controller方法中的形参绑定:
在这里插入图片描述

3.2 嵌套包装类型参数绑定

  在包装类中嵌套包装类,即Java实体类中包含其他其他实体类,此时Spring MVC依然可以解析并成功绑定该类型的包装类。

  例如查询用户姓名为“张三”,并且其名下的水果商品中产地包含“山东”的商品,此时就需要这样一个JavaBean来作为查询包装类UserAndProductModel,具体代码如下:

package com.ccff.model;

public class UserAndProductModel {

    private User user;
    private Fruits userFruits;

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public Fruits getUserFruits() {
        return fruits;
    }

    public void setUserFruits(Fruits fruits) {
        this.fruits = fruits;
    }
}

  UserAndProductModel实体类中又涉及到了另外两个实体类,分别为:Fruits和User实体类。Fruits实体类之前已经给出了详细定义,此处便仅给出User实体类的详细定义,具体代码如下:

package com.ccff.model;

public class User {
    private int userId;
    private String username;

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

  然后对之前的名为“findFruits.jsp”的JSP页面中的form表单进行修改。由于该查询中包装类包括了User类和水果商品Fruits类作为其属性,那么在进行查询时,指定input的name属性为“包装对象.属性”的形式。例如在下面的修改中,仅对水果产品信息进行查询,同时也可以对用户数据进行查询,它们会同时被绑定在 UserAndProductModel 对象中。具体代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>   
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
<html>  
<head>  
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
  <title>水果搜索</title>
</head>  
<body>
  <h3>水果搜索</h3>

  <form action="queryUserAndFruitsByCondition.action" method="post">
      名称:<input type="text" name="userFruits.name" />&nbsp;&nbsp;
      产地:<input type="text" name="userFruits.producing_area" />
      <input type="submit" value="搜索" />
  </form>

  <hr/>

  <h3>水果搜索结果</h3>

  <table width="300px;" border=1>  
    <tr>
      <td>编号</td>
      <td>名称</td>  
      <td>价格</td>    
      <td>产地</td> 
   </tr>  
   <c:if test="${findList==null}">
      <b>水果商品信息为空!</b>
   </c:if>
   <c:forEach items="${findList}" var="fruit">
     <tr>
       <td>${fruit.id }</td>
       <td>${fruit.name }</td>  
       <td>${fruit.price }</td>    
       <td>${fruit.producing_area }</td>  
     </tr>  
    </c:forEach>  
   </table>   
</body>
</html> 

  对JSP页面修改完毕后,接着,为了在Controller中拿到该类型,只需要使用UserAndProductModel作为其方法的形参即可,(前端页面中的表单input标签的name属性值的名与形参对象中的属性名可需要保持一致),具体代码如下:

package com.ccff.controller;

import com.ccff.model.Fruits;
import com.ccff.model.UserAndProductModel;
import com.ccff.service.FruitsService;
import com.ccff.service.FruitsServiceImpl;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/query")
public class UserAndFruitsController {
    private FruitsService fruitsService = new FruitsServiceImpl();

    @RequestMapping("/queryUserAndFruitsByCondition")
    public String queryUserAndFruitsByCondition(Model model, UserAndProductModel userAndProductModel){
        List<Fruits> findList = null;
        if (userAndProductModel == null){
            findList = fruitsService.queryFruitsList();
        }else if (userAndProductModel.getUserFruits() == null || (userAndProductModel.getUserFruits().getName() == null && userAndProductModel.getUserFruits().getProducing_area() == null)){
            //如果fruits或查询条件为空,默认查询所有数据
            findList = fruitsService.queryFruitsList();
        }else {
            //如果fruits查询条件不为空,按条件查询
            findList = fruitsService.queryFruitsByCondition(userAndProductModel.getUserFruits());
        }
        //将model数据传到页面
        model.addAttribute("findList",findList);
        return "fruits/findFruits";
    }
}

  当前端页面发出请求后,处理器适配器会解析这种格式的name,将该参数当做查询包装类的成员参数绑定起来,作为Controller方法的形参。这样在Controller方法中就可以通过查询包装类获取其包装类的其他类的对象。

  再次将项目部署到Tomcat上后,在浏览器上访问请求URL:http://localhost:8080/demo/query/queryUserAndFruitsByCondition.action ,同理第一次由于查询条件为空则显示所有水果信息。当在产地搜索框中输入“山东”后点击搜索按钮,则可以搜索出产地为 山东的所有水果信息,具体如图所示:
在这里插入图片描述

4、 集合类型参数绑定

4.1 数组类型的请求参数

4.2 List类型的请求参数

4.3 Map类型的请求参数

猜你喜欢

转载自blog.csdn.net/weixin_36378917/article/details/83660999