1.4 Spring MVC入门

HTTP

http协议是做web开发的基础,

  • HyperText Transfer Protocol(超文本传输协议)
  • 用于传输HTML等内容的应用层协议
  • 规定了浏览器和服务器之间如何通信,以及通信时数据的格式

手册:MND Web 文档
查看http相关手册

image.png

http概述

主要看这个协议到底规定了什么东西,重点列举以下两点:

  • http流
    下图是http协议规定的浏览器和服务器通信的步骤:
http流.png

HTTP/1.1以及更早的HTTP协议报文都是语义可读的。在HTTP/2中,这些报文被嵌入到了一个新的二进制结构,帧。帧允许实现很多优化,比如报文头部的压缩和复用。即使只有原始HTTP报文的一部分以HTTP/2发送出来,每条报文的语义依旧不变,客户端会重组原始HTTP/1.1请求。因此用HTTP/1.1格式来理解HTTP/2报文仍旧有效。
两种 HTTP报文的类型,请求与响应,每种都有其特定的格式。

第一种:浏览器向服务器发送的报文叫做请求

报文_请求.png

第二种:服务器向浏览器返回的报文叫做响应
报文_响应.png

详细手册查询见文章:HTTP查阅手册地址

其实我们通过观察浏览器插件或在具体应用过程中也会看到请求或响应相关的数据。打开浏览器的插件去看一下请求和响应的相关数据。
这里以chrom为例,右键--检查(N)--Network


浏览器插件_Network.png

刷新当前网页后,浏览器就访问了服务器,产生了通信,我们就能看到请求数据和响应数据了。如下图:
请求.png

点击查看第一条数据HTTP
http.png

General内容如下:
General内容.png

为什么有那么多次请求呢?
浏览器与服务器之间产生通信,浏览器访问服务器,服务器返回的是一个html,浏览器对html进行解读并渲染相关的内容,因为html可能多处用到css文件或js文件或图片文件或视频文件等,当发现依赖文件时便会向服务器提出请求,服务器便会再返回相应的html依赖的文件。关键在于网页(html文件)的返回是否存在问题。


多个请求.jpg

Spring MVC

我们做服务端开发是有层次的,如果不分层用一个类去写就会使耦合度很高,后期难以维护。服务端的代码分为三层。

  • 服务端三层架构

表现层、业务层、数据访问层

浏览器访问服务器首先访问的是表现层,期待表现层给其返回一些数据,表现层会调用业务层去处理业务,业务层在处理业务过程中会调用数据库。最终表现层得到业务层返回的数据后经过加工处理就返回给浏览器,完成整个请求。


三层架构的关系.png
  • MVC

MVC是一种设计模式,这种模式的理念是将复杂的代码分为三个层次
M--Model--模型层
V--View--视图层
C--Controller--控制层
MVC主要解决的是服务端表现层的问题,当浏览器发送请求访问服务器的时候,访问Controller控制器,Controller控制器会接收请求中的数据调用业务层处理请求,处理完成后将得到的数据封装到Model中,传给试图层View,视图层利用Model中的数据生成一个html返回给浏览器。浏览器最终通过试图层得到了html。总之,Controller用于处理浏览器的请求、负责调度,View负责渲染、展现,Controller和View之间的纽带是Model。

  • Spring MVC核心组件

前端控制器:DispatcherServlet(实际上是一个类)。M、V、C都是由这个类调度的。下面为Spring MVC的底层原理图,显示了服务器(Tomcat)内,前端控制器对Controller\Model\View的调度。(图片来源:spring.io--Spring Framework Reference Documentation of 4.3.26.RELEASE --Web MVC framework

Spring MVC底层原理图.png

总结要掌握的两点:
1.理解MVC模式解决的是哪个层次的问题、是如何解决的;
2.理解Spring MVC核心的组件是什么、是如何调度MVC三层的。

Thymeleaf

给浏览器返回动态网页,需要另外的工具去支持--模板引擎。目前最流行的模板引擎为Thymeleaf。

  • 模板引擎
    生成动态的HTML


    模板引擎的原理.png
  • Thymeleaf
    倡导自然模板,即以HTML文件为模板。
    有的模板引擎以jsp为模板,那不懂JavaScript只懂html的人就很尴尬,Thymeleaf纯html就很友好。

  • 常用语法
    无论用什么样的模板引擎,重点学的是三方面的内容:
    标准表达式(页面那些地方要被动态数据替换)、判断与循环(渲染数据过程中需要判断数据是否为空值、model给我的数据可能是一个集合或数组时就需要循环处理)、模板的布局(很多页面的结构是相似的,尽量将布局一样的区域复用)。

  • Thymeleaf官网:
    https://www.thymeleaf.org

案例演示

  • 配置Thymeleaf

在application.properties中配置。
Thymeleaf默认启用缓存,需要把其缓存关掉,原因是若不关掉模板缓存即使更改了页面但仍可能看到原来未更改的页面,存在延迟;但当系统上线后,模板的缓存就应该开启了,因为缓存会降低服务器的压力。在application.properties中加入代码:

spring.thymeleaf.cache=false

我们知道,spring boot火热的一个重要的原因是他的@SpringBootApplication注解,是一个复合注解,包含了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan注解。其中@EnableAutoConfiguration可以根据客户引入的spring boot的库来自动加载配置,但是到底加载哪些配置项呢?这些配置项到底属于哪个类呢?也就是说,我们知道spring的依赖注入的方式加载配置项,每个配置项必然注入某个类的属性,那么我们找到这些类,就能知道有哪些配置项可配了。

关于查阅application proprerties的配置手册:spring.io-->projects-->spring boot(Learn-->Reference Doc. )-->Application Properties-->查找Thymeleaf
由此可知thymeleaf缓存的配置项是spring.thymeleaf.cache。

application properties手册.png

在application.properties加入的配置其实是给一个配置类注入数据,比如我在aplication.properties中加入的spring.thymeleaf.cache=false其实是给thymeleaf相关的某个配置类的cache属性赋值为false,那么如何知道我使用的配置项属于哪个类呢?因为spring官网发布的新版本的spring boot的说明文档中的Common Application Properties页面显示的配置项是按照功能模块划分的,并没有给出某配置项具体属于哪个类。如图:

新版本spring boot的common application properties.png

因此查阅旧版本说明文档的Common Application Properties,网址:https://docs.spring.io/spring-boot/docs/2.1.13.RELEASE/reference/html/common-application-properties.html
配置项说明.jpg

旧版本中都标明了每个配置项的出处。
比如我只知道配置数据源使用spring.thymeleaf,但我还想知道spring.thymeleaf有哪些配置项,这些配置项注入到哪个底层类中,它告诉你了:


旧版本application properties.png

旧版本application properties2.png

那么我们已经知道thymeleaf配置项源于ThymeleafAutoConfiguration这个类,Idea中快捷键Ctrl+N查看该类。由该类中@EnableConfigurationProperties({ThymeleafProperties.class})可知详细的参数配置应该是ThymeleafProperties.class,那就进入ThymeleafProperties.class查看。

快捷键Ctrl+N.png

thymeleaf配置文件.png

配置好以后,下面写代码演示spring MVC常用的语法。因为spring MVC解决的是表现层的问题,表现层肯定要先写Controller,Model这个类本来就有就可以直接拿来用,再者就是写模板引擎所需要的模板,模板就放在templates中。项目包中三层架构的分布如下图。


项目包中三层架构的分布.png
以下所有代码实现都是在controller下的AlphaController中进行的。

演示在spring MVC框架下如何获得请求对象和响应对象。底层层面来讲,请求对象中封装了请求数据,响应对象中封装了响应数据,他们分别能够处理请求和响应。spring MVC本身有更简洁的方式处理问题,但是这里先要理解比较底层的机制,简洁的方式其实是对底层的组件、对象做封装。下面在AlphaController中写一个方法。

    @RequestMapping("/http")
    //若想获取请求对象、响应对象,只需在这个方法上加以声明,
    // 声明了这两个对象以后DispatcherServlet在调用这个方法的时候就会自动将这两个对象传过来
    //request对象常用的接口是HttpServletRequest
    public void http(HttpServletRequest request, HttpServletResponse response) {//这里没有返回值,原因是通过response对象可以直接向浏览器输出数据而不依赖返回值
        //获取请求数据request
        System.out.println(request.getMethod());
        System.out.println(request.getServletPath());//获取请求路径
        //以上两行为请求行数据
        Enumeration<String> enumeration = request.getHeaderNames();//得到所有请求行的key,因为有很多数据所以要用到迭代器enumeration
        while(enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            String value = request.getHeader(name);
            System.out.println(name + ":" + value);
            //以上为请求消息头,是若干行的数据。
        }
        // 除此以外还有请求体,包含业务数据、参数。
        // 在访问这个方法时若想传一个参数code进来,如何通过request得到参数?
        System.out.println(request.getParameter("code"));



        //向浏览器做出响应、返回响应数据response
        //首先设置返回的数据类型,是返回html、还是图片、字符串等
        //若返回网页
        response.setContentType("text/html;charset=utf-8");//text/html表示返回网页类型的文本,charset=utf-8以便于支持中文
        //通过封装的输出流向浏览器输出
        try(//java7新语法:在try后加一个小括号,writer在小括号内创建,编译时会自动加一个finally,然后再close
                PrintWriter writer = response.getWriter();//获取输出流
        ) {
            writer.write("<h1>乐坛网</h1>");//通过writer向浏览器打印一个网页,输出一个一级标题
     } catch (IOException e) {
            e.printStackTrace();
        }
    }

选中核心类CommunityApplication运行,打开浏览器访问http://localhost:8080/community/alpha/http

返回的乐坛网.png

在当前网页右键-->检查,在插件中Network界面-->刷新,发现只返回了一个请求,因为网页上没有依赖任何资源。
响应的一级标题.png

回到控制台看日志,发现打印结果与代码对应,如下:

控制台日志.png

如果我想传一个参数,那么就在原浏览器地址后加?参数名=参数值,多个参数就是?参数名1=参数值1&参数名2=参数值2,如:http://localhost:8080/community/alpha/http?code=123
http://localhost:8080/community/alpha/http?code=123&name=Joy
若输入地址为localhost:8080/community/alpha/http?code=123,回车后相应地控制台原来显示null的一行会变成123。

以上即为通过request对象获取相关数据并响应浏览器的底层原理,下面演示实际开发中处理问题的方法
处理浏览器的请求分为两个方面:请求处理&返回响应

请求处理方式:

请求方式有很多种,但是我们只需要用到get和post就能解决一切问题。
1.浏览器向服务器获取数据 GET请求

  • 方法一:
    //第一种方法:
    //GET请求,希望向服务器获取数据

    //假设我要查询所有学生,查询的路径为/students?current=1&limit=20。
    // 查出来的信息可能会很多,分页显示需要带上条件,告诉服务器当前(current)是第几页,每一页最多显示多少条数据(20条)
    //下面演示服务器怎样去处理这样的请求
    @RequestMapping(path = "/students", method = RequestMethod.GET) //强制只能处理get请求
    @ResponseBody
    public String getStudents(
            //注解@RequestParam对参数的注入作更详尽的声明
            //required=false表明不传这个参数也不会出错,如果不传这个参数,则默认值defaultValue是1
            @RequestParam(name = "current", required = false, defaultValue = "1") int current,
            @RequestParam(name = "limit", required = false, defaultValue = "10") int limit) {
        System.out.println(current);
        System.out.println(limit);
        return  "some students";
    }

重新编译运行结果为
students1.png

students控制台默认值.png

students2.png

students控制台 赋值参数.png
  • 方法二:
    //第二种方法:
    //根据学生编号查询一个学生 /student/123(123为学生编号)
    //直接把参数编排到路径当中成为路径的一部分
    @RequestMapping(path = "/student/{id}", method = RequestMethod.GET)
    @ResponseBody
    public String getStudent(@PathVariable("id") int id) {//注解@PathVariable表示路径变量

        return "a student";

    }

运行结果:


astudent页面.png

astudent控制台.png

总之,在get请求当中,有两种传参的方式,一种是?参数=参数值,一种是把某一参数直接加到路径当中。

2.浏览器向服务器提交数据 POST请求
浏览器向服务器提交数据,首先浏览器需要打开一个带有表单的网页,通过表单填写数据以后才能提交给服务器,现在还没有网页,那么我们就在项目中创建一个静态的网页。templates里存放的模板是动态资源,静态的资源应该放在static下。在static下建一个目录html,在static.html目录下创建一个html文件命名为student。
创建html静态页面

<!--新增一个学生发送给浏览器-->
<!--注释-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>增加学生</title>
</head>
<body>

    <!--在body之内加一个表单-->
    <form method="post" action="/community/alpha/student">
    <!--声明该表单用什么样的方式提交(post请求提交数据)、提交给谁(提交路径)-->
        <p>
            姓名:<input type="text" name="name">
            <!--文本框中的数据在填完提交给服务器时需要给这个数据起一个参数名-->
        </p>
        <p>
            年龄:<input type="text" name="age">
        </p>
        <p>
            <input type="submit" value="保存">
            <!--提交按钮-->
        </p>
    </form>

</body>
</html>

重新编译运行,浏览器中访问html页面,因为这个html放在static目录下,static这一级路径不用敲,需要敲入/html目录,因为访问的是静态页面所以与alpha目录没有任何关系。因此访问地址为:localhost:8080/community/html/student.html
访问结果如图


html静态网页.png

先不着急在表单中填数据,因为还没有在服务端加路径以处理请求。

补充:get请求也可以向服务器端传数据,那为什么不用get请求去传?因为get请求在传参数是,参数会直接显示在路径中,其次路径的长度是有限制的,若参数有很多项时get请求很可能传不下。因此在提交数据时一般不用get请求。

下面写方法来处理post请求

    //POST请求
    @RequestMapping(path ="/student", method = RequestMethod.POST)
    @ResponseBody
    public String saveStudent(String name, int age) {
    //直接声明参数,参数名字与表单中数据名一致就会自动把数据传过来
        System.out.println(name);
        System.out.println(age);
        return "success";
    }

重新编译运行,并刷新刚才的静态表单页面,在表单中填写数据并提交。


填写表单.png

表单提交结果.png

控制台打印.png

以上请求处理方式就演示完成了。接下来演示如何向浏览器返回响应数据。

返回响应方式:

之前我们都是响应字符串这种简单的内容,那么下面演示如何向浏览器响应动态的html。

  • 方法一:
    //响应HTML数据

    //假设浏览器查询一个老师,那么服务器就帮助其查询到了该老师相关的数据,并将数据以网页的形式响应给浏览器
    @RequestMapping(path ="/teacher", method = RequestMethod.GET)
    //不加注解@ResponseBody就默认返回html
    public ModelAndView getTeacher() {  //返回的数据类型是model和view两份数据,是提供给DispatcherServlet的
        ModelAndView mav = new ModelAndView();//先实例化
        mav.addObject("name", "张三");    //传入动态值
        mav.addObject("age", "30");
        mav.setViewName("/demo/view");
        //设置模板的路径和名字,模板都放在templates目录下,我用到的模板会放在templates的demo目录下,且目录后要跟上模板的文件名
        //因为thymeleaf引擎默认的模板就是html文件,因此不再需要写后缀,只需写文件名如:view.html只用写view
        return mav;

    }

代码中getTeacher()返回的数据类型是ModelAndView,这里就需要再复习一下spring MVC的原理:所有的组件都是由DispatherServlet去调度的,DispatherServlet会调用Controller的某个方法,这个方法需要给其返回model数据、视图相关数据,然后再由DispatherServlet把model和view提交给模板引擎,由模板引擎进行渲染生成动态html。

还缺templates.demo下的view.html模板,创建vie.html,代码如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--声明当前文件是一个模板而不是一个普通的html文件,这个模板的语法来源于thymeleaf官网-->
<!--  -->
<head>
    <meta charset="UTF-8">
    <title>Teacher</title>
</head>
<body>
    <p th:text="${name}"></p>
    <p th:text="${age}"></p>
    <!-- 第一段落:姓名,第二段落。因为姓名和年龄都是动态的值,我们需要利用模板引擎的语法去解决 -->
    <!--th:text="${name}"是thymeleaf的语法-->
</body>
</html>
响应结果以及插件检查.png
  • 方法二:
    上面的方法一更直观,是把model和view都装到了一个对象里,而此方法是把model数据装到参数中,把view视图直接返回,返回的值给了DispatcherServlet,同时DispatcherServlet也持有model的引用,因此DispatcherServlet依旧能得到model和view。
    //查询学校
    @RequestMapping(path ="/school", method = RequestMethod.GET)
    public String getSchool(Model model) {
    //这个model对象不是我们自己创建的,而是DispatcherServlet在调用方法时,当检索到有model对象时,自动实例化了model对象
    //model是一个bean,DispatcherServlet持有对bean的引用,在方法内部往bean中存数据也能够实现的
        model.addAttribute("name", "北京大学");
        model.addAttribute("age", "100");
        return "/demo/view";//返回view的路径
        //因为返回的类型是string,model数据如何传呢?加类型为model的参数
    }
方式二.png

对比来看方式二更加简洁,所以最好掌握这种方法。

  • json响应
    单个json对象
    除了能响应html以外,服务器还能向浏览器响应任何数据,常见响应JSON数据,通常是在异步(当前网页不刷新但是悄悄访问了服务器一次,得到了一个结果,结果不是html只是一个判断结果)请求当中。比如注册b站账号,填写用户名完成时光标切换,就会被判断出用户名是否可用。现在重点关注如何向浏览器响应这种数据。
    JSON数据是什么数据?有怎样的价值?java是面向对象的语言,得到的是java对象,将java对象数据返回给浏览器;浏览器解析对象用的是js,js也是面向对象语言,js希望得到js对象,但是Java对象不可能直接转成js对象,那么通过JSON就可以实现两者的兼容,JSON实际上是具有特定格式的字符串,Java对象可以转成字符串传给浏览器,浏览器可以将字符串转成js对象。因为任何语言都有字符串类型,字符串的格式是比较通用的格式,任何语言都能进行解析,都能将字符串转成对象,所以JSON起到了衔接的作用。因此在跨语言的情况下,json是常见的一种字符串形式,尤其是在异步请求当中,客户端需要返回一个局部验证的结果时,json响应就很方便。下面演示如何向浏览器响应json。
    //响应JSON数据(异步请求)
        @RequestMapping(path = "/emp", method = RequestMethod.GET)
        @ResponseBody
        public Map<String, Object> getEmp() {   //map用于封装
            Map<String, Object> emp = new HashMap<>();//实例化
            emp.put("name", "张三");
            emp.put("age", "23");
            emp.put("salary", "8000");
            return emp;

DispatcherServlet在调用getEmp()方法时,看到加了@ResponseBody注解以及返回的类型,就会自动把map转成字符串并发送给浏览器。

Json响应.png

插件_响应类型.png

多个json对象

    @RequestMapping(path = "/emps", method = RequestMethod.GET)
    @ResponseBody
    public List<Map<String, Object>> getEmps() {   //map用于封装
        List<Map<String, Object>> list = new ArrayList<>(); //创建一个list(集合)

        Map<String, Object> emp = new HashMap<>();//实例化
        emp.put("name", "张三");
        emp.put("age", "23");
        emp.put("salary", "8000.00");
        list.add(emp);//向集合内装数据

        emp = new HashMap<>();
        emp.put("name", "李四");
        emp.put("age", "24");
        emp.put("salary", "9000.00");
        list.add(emp);//向集合内装数据

        emp = new HashMap<>();
        emp.put("name", "王五");
        emp.put("age", "25");
        emp.put("salary", "10000.00");
        list.add(emp);//向集合内装数据



        return list;
    }
集合形式的json.png

以上就是在Spring MVC当中处理请求、处理响应的最常用的几种方式,后面项目中会经常反复用到。

End

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容

  • Web 页面的实现 Web 基于 HTTP 协议通信 客户端(Client)的 Web 浏览器从 Web 服务器端...
    毛圈阅读 1,083评论 0 2
  • 1.TCP报头格式 UDP报头格式 TCP报头格式 UDP报头格式 具体的各部分解释看 TCP报文格式详解 - ...
    杰伦哎呦哎呦阅读 2,454评论 0 5
  • 今天是新生报到的日子,马上就知道分班情况,见到新老师、新同学啦,爸爸妈妈亲自送孩子去学校报到,一路上孩子高兴地哼唱...
    不焦虑的小一阅读 59评论 0 0
  • 一、东野奎五曾写下“生命中的全部偶然,其实全都是命中注定,是为宿命”。我们总是在错误的时间,错误的地点,懵懵懂懂的...
    青春小女子阅读 479评论 2 16
  • 蝗 虫 来 了 顾 冰 蝗虫大军来了, 它成群结队从非洲出发, ...
    牛牛红红阅读 489评论 0 7