Servlet技术之——概述、实现、细节、获取资源、ServletConfig、ServletContext
Servlet概述、实现、细节、获取资源、ServletConfig、ServletContext
(一) Setvlet基本概述
(1) 什么是Servlet ?
Servlet(Server Applet)是JavaServlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容是
JavaWeb中,我们将会接触到三大组件(Servlet、Filter、Listener),Servlet由服务器调用,处理服务器接收到的请求,即完成,接受请求数据 --> 处理请求 --> 完成响应,其本质就是一个实现了Servlet接口的java类
Servlet类由我们来写,但对象由服务器来创建,并且由服务器来调用相应的方法
(2) Servlet用来做什么?
网络中比较常见的一些功能,例如登录,注册,这样存在交互的功能,而Servlet就可以帮助我们处理这些请求,可以说Servlet是JavaWeb知识中重要的知识点之一
(二) 实现Servlet的方式
实现Servlet有三种方式:
- 实现 javax.servlet.Servlet 接口;
- 继承 javax.servlet.GenericServlet类;
- 继承 javax.servlet.http.HttpServlet类;
实际开发中,我们通常会选择继承HttpServlet类来完成我们的Servlet,但认识Servlet接口这种方式也是很重要的,是我们入门知识中不可或缺的部分
(1) 创建我们的第一个Servelt
我们创建一个web项目,选择对应的参数,我们所装的jdk为1.8版本,可以选择到 JavaEE8的版本,对应versions也就是4.0,不过我们在这里选择市面上用的还是比较多的7版本
创建一个Demo类实现Servlet接口,然后我们快速生成这个接口中未实现的方法,我们先暂时忽略Servlet中其他四个方法,只关心service()方法,因为它是用来处理请求的方法,我们在该方法内给出一条输出语句
package cn.ideal.web.servlet;
import javax.servlet.*;
import java.io.IOException;
public class ServeltDemo1 implements Servlet {
//初始化方法
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
//Servlet配置方法
@Override
public ServletConfig getServletConfig() {
return null;
}
//提供服务方法
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("理想二旬不止");
}
//Servlet信息方法
@Override
public String getServletInfo() {
return null;
}
//销毁方法
@Override
public void destroy() {
}
}
写完了一个最简单Servlet代码,但是如何在浏览器中可以访问到呢?我们就需要对web/WEB-INF下的web.xml进行配置,我们在<web-app></web-app>
中加入以下代码(虽然后期有优化的方法,但是很推荐大家记忆下来)
<servlet>
<!--给这个Servlet起一个名字,一般与类名相同-->
<servlet-name>ServletDemo1</servlet-name>
<!--全类名-->
<servlet-class>cn.ideal.web.servlet.ServeltDemo1</servlet-class>
</servlet>
<!--配置映射路径-->
<servlet-mapping>
<servlet-name>ServletDemo1</servlet-name>
<!--外界访问的路径-->
<url-pattern>/Demo1</url-pattern>
</servlet-mapping>
现在我们根据我们在url-pattern
中配置的路径来访问一下,在控制台中果然输出了,理想二旬不止这个字符串
(2) web.xml的作用
趁热打铁,我们来简单分析一下这个web.xml的因由,其实在web.xml中配置Servlet的目的,就是把在浏览器中的访问路径与对应Servlet绑到一起,上面的例子中就是把访问路径:“/Demo1” 与 “cn.ideal.web.servlet.ServeltDemo1” 绑定到了一起
1、<servlet></servlet>
:指定ServletDemo1这个Servlet的名字为ServletDemo1,一般此处与对应类同名
2、<servlet-mapping></servlet=mapping>
:设定访问的具体路径
而这两者又通过 <servlet-name></servlet-name>
关联在一起
执行过程:
1、当服务器中接受到了浏览器的请求,解析URL路径,获取到Servlet的资源路径
2、寻找web.xml文件,找到 <url-pattern>
标签,寻找对应的全类名 <servlet-class>
3、Tomcat将字节码文件加载进内存,并且创建对象,调用其中的方法
所以我们需要知道:Servlet中的大多数方法均不由我们来创建和调用,均由Tomcat完成
(三) Servlet 接口
(1) 生命周期简单概述
我将生命周期简单理解为这样几个过程:
生前——出生——服务——死亡——埋葬
1、生前:当Tomcat第一次访问Servlet,Tomcat会创建Servlet的实例
2、出生:Tomcat会调用init()方法初始化这个对象
3、服务: 客户端访问Servlet的时候,service()方法会被调用
4、死亡: 当Tomcat被关闭或者Servlet长时间不被使用,destroy()方法会被调用
5、埋葬: destroy()方法被调用后,Servlet就等待垃圾回收(不轻易),有需要则用init()方法重新初始化
(2) 生命周期详解
1、生前
服务器会在Servlet第一次被访问时,或者是在服务器启动时创建Servlet。如果服务器启动时就创建Servlet,那么还需要在web.xml文件中进行配置,也就是说默认情况下,Servlet是在第一次访问时由服务器创建的
一个Servlet类型,服务器只创建一个实例对象:例如我们第一次访问<http://localhost:8080/Demo1>
,服务器通过/Demo1
就找到了cn.ideal.web.servlet.ServeltDemo1
,服务器就会判断这个类型的Servlet是否创建过,若没有才通过反射来创建ServletDmoe1的实例,否则就直接使用已经存在的实例
2、出生
在Servlet被创建后,服务器会立即调用Servlet的void init(ServletConfig)
方法,而且一个Servlet的一生,这个方法只会被调用一次,我们可以把一些对Servlet的初始化工作放到方法中!
3、服务
当服务器每次接收到请求时,都会去调用Servlet的service()方法来处理请求。service()方法是会被调用多次的,服务器接收到一次请求,就会调用service() 方法一次,也正因为如此,所以我们才需要把处理请求的代码写到service()方法中!
4、死亡及埋葬
当服务器关闭时Servlet也需要被销毁了,但是销毁之前,服务器会先调用Servlet中的destroy()方法,我们可以把一些释放资源的代码放到此处
(3) Servlet接口的三个类型
在这五个方法中,我们可以在参数中看到三个我们没有接触过的类型
public void init(ServletConfig servletConfig) throws ServletException {}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {}
也就是这个三个类型:ServletConfig、ServletRequest、ServletResponse
A:ServletConfig
ServletConfig是服务器创建的一个对象,然后传递到Servlet的init()方法中
下述方法中我们简单使用一下第一个 getServletName() 就可以了,后面的方法等我们学写了Context以及其他知识才能更好的理解
//获取Servlet在web.xml文件中的配置名称,即<servlet-name>指定的名称
String getServletName()
//用来获取ServletContext对象
ServletContext getServletContext()
//用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值;
String getInitParameter(String name)
//用来获取在web.xml中配置的所有初始化参数名称
Enumeration getInitParameterNames()
B:ServletRequest & ServletResponse
这两个类型出现在Servlet的service()方法中,分别代表着请求与响应对象,并且两者的实例也都是由服务器创建的
但是我们想要做一个web应用,归根结底要和HTTP相挂钩,如果我们希望在service() 方法中使用HTTP相关的功能,可以把 ServletRequest 和 ServletResponse 强转成 HttpServletRequest 和HttpServletResponse
HttpServletRequest方法:
//获取指定请求参数的值;
String getParameter(String paramName)
//获取请求方法,例如GET或POST
String getMethod()
//获取指定请求头的值;
String getHeader(String name)
//设置请求体的编码!
/*
GET没有请求体,所以这个方法只只对POST请求有效当调用
这个方法必须在调用getParameter()方法之前调用!
使用request.setCharacterEncoding(“utf-8”)之后,再通过getParameter()方法获取参数
时,参数值都已经通过了转码,即转换成了UTF-8编码
*/
void setCharacterEncoding(String encoding)
HttpServletResponse方法:
//获取字符响应流,使用该流可以向客户端输出响应信息。
PrintWriter getWriter()
Eg:response.getWriter().print(“<h1>Just for test</h1>”);
//获取字节响应流,例如可实现向客户端响应一张图片
ServletOutputStream getOutputStream()
//用来设置字符响应流的编码
void setCharacterEncoding(String encoding)
//向客户端添加响应头信息
void setHeader(String name, String value)
Eg:setHeader(“Refresh”, “3;url=http://www.xxx.com”) 表示三秒后自动刷新到该网址
//该方法是setHeader(“content-type”, “xxx”)的简便方法,即用来添加名为content-type响应头的方法
/*
content-type响应头用来设置响应数据的MIME类型,例如要向客户端响应jpg的图片,那么
可以setContentType(“image/jepg”),如果响应数据为文本类型,那么还要台同时设置编
码,例如setContentType(“text/html;chartset=utf-8”)表示响应数据类型为文本类型
中的html类型,并且该方法会调用setCharacterEncoding(“utf-8”)方法;
*/
void setContentType(String contentType)
//向客户端发送状态码,以及错误消息
void sendError(int code, String errorMsg)
(四) GenericServlet 类
A:通过查看这个类的源码可以知道,该类中只有
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
一个方法需要实现,其他的方法已经均在源码中有了定义
B:GenericServlet的init()方法
还需要提一提的两个方法就是
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
GenericServlet 类实现了Servlet的init(ServletConfig)方法,把参数config赋给了本类的成员config,然后再调用本类自己的无参的init()方法
这个方法是 GenericServlet 自己的方法,而不是从Servlet继承下来的。当我们自定义Servlet时,如果想完成初始化作用就不要再重复 init(ServletConfig) 方法了,而是应该去重写init()方法。因为在 GenericServlet中的 init(ServletConfig) 方法中保存了 ServletConfig 对象,如果覆盖了保存 ServletConfig 的代码,那么就不能再使用 ServletConfig 了
C:实现了ServletConfig接口
GenericServlet还实现了ServletConfig接口,所以可以直接调用getInitParameter()、getServletContext()等ServletConfig的方法。
但是这个类我们仍然不是我们要讲的重点,我们接着看一下下一个类
(五) HttpServlet 类
(1) 概述
在上面我们实现 Servlet 接口,需要实现5个方法,十分麻烦,而HttpServlet类已经实现了Servlet接口的所有方法,编写Servlet时,只需要继承HttpServlet,重写你需要的方法即可,并且它提供了对HTTP请求的特殊支持,更加强大
(2) service()方法
在 HttpServlet 的 service(ServletRequest,ServletResponse)
方法中会把 ServletRequest 和ServletResponse 强转成 HttpServletRequest 和 HttpServletResponse
//HttpServlet 源码节选
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException("non-HTTP request or response");
}
this.service(request, response);
}
强转过后,然后调用 HttpServlet 类中提供的 service(HttpServletRequest,HttpServletResponse)
方法,这是这个类本身的方法,而不是继承而来的,这说明我们在使用的时候,只需要覆盖 service(HttpServletRequest,HttpServletResponse)
就可以了,不需要再进行强转这个两个对象了
注意:其实还有更一步简化的步骤,也不必使用 service(HttpServletRequest,HttpServletResponse)
(3) doGet() 和 doPost()
在HttpServlet的service(HttpServletRequest,HttpServletResponse)
方法会去判断这个请求是GET还是POST,如果是GET请求,就去调用类中的doGet()方法,如果是POST请求,就去调用doPost()方法,这说明我们在子类中去覆盖doGet()或doPost()方法就可以了
(六) Servlet细节
(1) 线程安全问题
Servlet只会被服务器创建一个实例对象,很多情况下,一个Servlet需要处理多个请求,显然,Servlet虽然效率高,但也不是线程安全的
所以我们不应该在Servlet中轻易创建成员变量,因为可能会存在多个线程同时对这个成员变量进行不同的操作
结论:不要在Servlet中创建成员!创建局部变量即可,可以创建无状态成员量,或者状态只为可读的成员
(2) 服务器启动时就创建Servlet
之前我们将生命周期的时候有说过,Servlet是在第一次访问时由服务器创建的,但我们可以通过在web.xml中对Servlet进行配置,使服务器启动时就创建Servlet
<servlet>
<servlet-name>ServletDemo1</servlet-name>
<servlet-class>cn.ideal.web.ServletDemo1</servlet-class>
<!--在<servlet>中配置<load-on-startup>,其中给出一个非负整数!-->
<load-on-startup>0</load-on-startup>
</servlet>
它的作用是确定服务器启动时创建Servlet的顺序
(3) 一个Servlet可以绑定多个URL
<servlet-mapping>
<servlet-name>Servlet</servlet-name>
<url-pattern>/AServlet</url-pattern>
<url-pattern>/BServlet</url-pattern>
</servlet-mapping>
这样配置后无论访问/AServlet还是/BServlet,访问的都是AServlet
(4) 通配符匹配问题
在<url-pattern>
中可以使用通配符,也就是 “ * ” ,它可以匹配任何前缀或者后缀
<!--路径匹配-->
<url-pattern>/servlet/*<url-patter>:/servlet/a、/servlet/b,都匹配/servlet/*;
<!--扩展名匹配-->
<url-pattern>*.xx</url-pattern>:/abc/de.xx、/a.xx,都匹配*.xx;
<!--什么都匹配-->
<url-pattern>/*<url-pattern>:匹配所有URL;
通配符要么为前缀,要么为后缀,不能出现在URL中间位置,并且一个URL中最多只能出现一个通配符,如果存在更具体的地址,会优先访问具体的地址
(七) ServletContext
(1) 概述
服务器会为每个web应用创建一个 ServletContext 对象,可以说它就代表着这个web站点,并且这个对象,在Tomcat启动时就创建,在Tomcat关闭时才会销毁
(2) 功能
所有Servlet都共享着一个ServletContext对象,所以ServletContext对象的作用是在整个Web应用的动态资源之间共享数据,也就是说不同的Servlet之间可以通过ServletContext进行通讯,从而共享数据
(3) 获取ServletContext对象
GenericServlet类有getServletContext()方法,所以可以直接使用this.getServletContext()来获取
public class MyServlet implements Servlet {
public void init(ServletConfig config) {
ServletContext context = config.getServletContext();
}
}
public class MyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) {
ServletContext context = this.getServletContext();
}
}
(4) 域对象的功能
所有域对象都有存取数据的功能,可以将这种存储数据的方式看做,Map的方式
我们来看几个常见的用来操作数据的方法
存储
//用来存储一个对象,也可以称之为存储一个域属性
void setAttribute(String name, Object value)
Eg:servletContext.setAttribute(“xxx”, “XXX”)
//在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX
获取
//用来获取ServletContext中的数据
Object getAttribute(String name)
//获取名为xx的域属性
Eg:String value = (String)servletContext.getAttribute(“xxx”);
//获取所有域属性的名称;
Enumeration getAttributeNames()
移除
//用来移除ServletContext中的域属性
void removeAttribute(String name)
访问量统计的小案例
package cn.ideal.web.servlet;
import javax.servlet.*;
import java.io.IOException;
public class ServletDemo2 extends GenericServlet {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
//获取ServletContext对象
ServletContext servletContext = this.getServletContext();
//获取ServletContext对象中的count属性
Integer count = (Integer) servletContext.getAttribute("count");
if (count == null) {
//如果在ServletContext中不存在count属性,name设置为count的值为1,表示第一次访问
count = 1;
} else {
//如果在Servlet中存在count属性,说明以前被访问过,name让count在原来的基础上加1
count++;
}
servletResponse.setContentType("text/html;charset=UTF-8");
//向客户端响应本页面被访问的次数
servletResponse.getWriter().print("<h1>本页面一共访问" + count + "次</h1>");
//保存count的值到ServletContext对象中
servletContext.setAttribute("count", count);
}
}
(八) 获取资源相关方法
(1) 获取路径
使用ServletContext对象可以用来获取Web应用下的资源,例如在一个web应用的根目录下创建aaa.txt文件,WEB-INF目录下创建bbb.txt文件,如果我们想要通过Servlet获取这两者的路径就可以这样来写
//获取aaa.txt的路径
String realPath = servletContext.getRealPath(“/aaa.txt”)
//获取bbb.txt的路径
String realPath = servletContext.getRealPath(“/WEB-INF/b.txt”)
获取单个文件路径是这样,我们还有一种方式,可以获取到指定目录下所有的资源路径,例如获取/WEB-INF下的所有资源路径
Set set = context.getResourcePaths("/WEB-INF");
System.out.println(set);
(2) 获取资源流
不仅我们可以使用ServletContext获取路径,我们还可以获取资源流,以上面假设的两个文件为例
//获取aaa.txt
InputStream in = servletContext.getResourceAsStream(“/aaa.txt”);
//获取bbb.txt
InputStream in = servletContext.getResourceAsStream(“/WEB-INF/b.txt”);
(3) 获取类路径下资源
InputStream in = this.getClass().getClassLoader().getResourceAsStream("xxx.txt");
System.out.println(IOUtils.toString(in));
(九) 使用注解,不再配置web.xml
每创建一个Servlet我们就需要在web.xml中配置,但是如果我们的Servlet版本在3.0以上,就可以选择不创建web.xml,而使用注解来解决,十分简单方便
例如我们创建一个Servlet,配置web.xml如下
<servlet>
<servlet-name>ServletDemo2</servlet-name>
<servlet-class>cn.ideal.web.servlet.ServletDemo2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServletDemo2</servlet-name>
<url-pattern>/Demo2</url-pattern>
</servlet-mapping>
//在类名的上方写入这样一句代码,引号内为外部访问路径
@WebServlet("/Demo2")
是不是很简单方便,我们看一下其中的原理:
//WebServlet 源码节选
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
String name() default "";
String[] value() default {};
String[] urlPatterns() default {};
int loadOnStartup() default -1;
这个注解可以看到,@Target({ElementType.TYPE})
作用范围为类上,@Retention(RetentionPolicy.RUNTIME)
保留在运行期,name()方法反而在这里没有那么重要,因为在web.xml中,name主要起一个关联的作用,其中我们最重要的就是这个String[] urlPatterns() default {};
配置一个地址,它的定义为一个数组,当然配置一个也是可以的,即urlPatterns = "/Demo2"
而其中value所代表的最重要的值,其实也就代表这个地址,所以可以写为 Value = "/Demo2"
,而 Value又可以省略,所以可以写成 "/Demo2"
结尾:
如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家 !_
如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)
在这里的我们素不相识,却都在为了自己的梦而努力 ❤
一个坚持推送原创Java技术的公众号:理想二旬不止
Servlet技术之——概述、实现、细节、获取资源、ServletConfig、ServletContext的更多相关文章
- web开发技术中Servlet技术的概述
1.servlet是什么:servlet是一个位于服务器端的java应用程序它可以像jsp一样,直接输出信息 servlet类必须继承HttpServlet类,否则,不能称为serlvet servl ...
- Servlet --启动时创建、配置url、ServlectContext、初始化参数、获取资源
servlet的版本的区别 2.5版本, Servlet的配置只支持在xml文件中的配置 3.0版本: Servlet的配置支持在xml文件中的配置, 也可以使用注解的方式, 默认使用注解 让服务器在 ...
- Java Servlet 技术简介
Java Servlet 技术简介 Java 开发人员兼培训师 Roy Miller 将我们现有的 servlet 介绍资料修改成了这篇易于学习的实用教程.Roy 将介绍并解释 servlet 是什么 ...
- JavaWeb开发之四:servlet技术 黑马程序员_轻松掌握JavaWeb开发之四Servlet开发 方立勋老师视频教程相当的经典
总结: 记住:servlet对象在应用程序运行的过程中只创建一次,浏览器每次访问的时候,创建reponse对象 request对象,然后调用servlet的service方法,reponse对象和re ...
- Java Web之Servlet技术
1.Servlet基础 针对Servlet技术开发,Sun公司提供了一些列接口和类,其中最重要的是javax.servlet.Servlet接口,两个重要的包是javax.servlet和javax. ...
- [原创]java WEB学习笔记69:Struts2 学习之路-- 消息处理与国际化,概述,配置国际资源文件,访问国际化消息,通过超链接切换语言
本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...
- JavaWeb:Servlet技术
JavaWeb:Servlet技术 快速开始 Servlet是什么 Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 ...
- Tomcat 学习总结(1) --Servlet技术
在Web应用中,Servlet是一门重要的技术,Servlet是利用Java类编写的服务端程序,与平台架构,协议无关. JSP的实质就是Servlet,因为所有的JSP页面传回服务端时要转为Servl ...
- 01 Servlet技术
Servlet 1.Servlet技术 1.1.什么是Servlet Servlet是JavaEE规范之一.规范就是接口 Servlet就JavaWeb三大组件之一.三大组件分别是:Servlet程序 ...
随机推荐
- 使用Ajax和一般处理程序实现文件上传与下载
1.使用HTML的input标签 <input type="file" multiple="multiple" id="file_load&qu ...
- 分治FFT学习笔记
用途 在\(O(n\log^2 n)\)的时间内做诸如 \[ f_n=\sum_{i=0}^{n-1} f_ig_{n-i} \] 或是 \[ f_n=\sum_{i=0}^{n-1} f_if_{n ...
- WallpaperEngine 导入 mp4 文件出现 failed opening video
WallpaperEngine 导入 mp4 文件出现 failed opening video 如图 下载这个插件 链接:点击打开链接 密码:dkf8
- 函数第二部分:为什么说动态参数是没有计划好的参数-Python基础前传(11)
动态参数1-一个星号变元组 动态参数存在的意义? 函数的作者有时候也不知道这个函数到底需要多少个参数,这时候动态参数就有存在的意义了 动态参数创建-加* 底层原理是:把数值型或其他数据类型变成了元组类 ...
- springboot项目整合swagger2出现的问题
swagger需要开放以下uri:/swagger-ui.html/swagger-resources/webjars/csrf/v2 添加swagger后项目报错 Failed to start b ...
- python3编程基础之一:代码封装
几乎现代的编程语言都支持函数,函数是代码段的封装,并能实现一特定功能,并能重复使用的代码单位.之前的pow()和sqrt()和print()和input()等类似的内置函数,就是python内部已经实 ...
- chrome浏览器备忘
记录日常使用Chrome遇到的问题. audio控件播放音频问题 打开http://www.cdfive.com,发现音乐没有自动播放,F12打开控制台发现有如下报错: Uncaught (in pr ...
- Java打印素数(质数)
要求:打印 2 - 100000 当中的素数与非素数.(素数定义:在大于1的自然数中,除了1和它本身以外不再有其他因数) 1. 常规方式——对正整数n,如果用2到 之间的所有整数去除,均无法整除,则 ...
- Windows服务器下,Tomcat制作成服务自启动,跑不起来,报内存溢出。
测试过不行 使用服务启动后,tomcat运行时会抛出各种内存溢出错误,于是我第一时间想到的是去修改catalina.bat文件中的内存设置,但是然并卵,于是我又尝试不用服务,去使用startup.ba ...
- ElementUI】日期选择器时间选择范围限制,只能选今天之前的时间,或者是只能选今天之后的时间。今天是否可以选。限制结束日期不能大于开始日期
<el-date-picker v-model="value1" type="date" placeholder="选择日期" :pi ...