javaWeb学习之jsp
1.jsp初识
概述:JSP就是Java Server Pages(java服务器端页面),其实就是在HTML中嵌入java代码
为什么要学习jsp
SUN公司提供了动态网页开发技术:Servlet,但是,Servlet自身有一些缺点,sun公司发现了这些问题,就提出了一个新的动态网页开发技术jsp
Servlet的缺点:
- Servlet需要进行配置,不方便维护
- Servlet很难向网页中输出HTML页面内容
jsp的运行原理
jsp文件被翻译成了java文件,这个java文件就是一个Servlet,将这个java文件编译生成class文件,运行class文件
jsp的注释:
1.HTML部分代码的注释:
2.java代码的注释:<%
//单行注释
/*多行注释*/
/**文档注释*/ %>
3.jsp自己独有的注释:<%--jsp的注释--%>
jsp开发模式的发展(图片来源于黑马视频教程截图)
jsp实现请求转发
前面我们的MVC开发模式中Servlet负责控制调度,也就是实现请求的转发,那我们怎么实现请求转发呢?想要学习怎么请求转发,就得先来看看两个方法:
- getRequestDispatcher(String path):通过Request对象获取RequestDispatcher对象
- forword(ServletRequest request, ServletResponse response):通过RequestDispatcher对象进行请求转发
补充:通过response对象的重定向方法sendRedirect()也可以实现页面的跳转,但是重定向与请求转发是有一定区别的
请求转发与重定向的区别:
- 请求转发是一次请求一次响应,而重定向是两次请求两次响应
- 请求转发地址栏地址是不会变化的,而重定向地址栏地址是会发生变化的
- 请求转发路径不带工程名,重定向需要带工程名路径
- 请求转发只能在本网站内部,重定向可以定向到任何网站
2.jsp的脚本元素、指令元素
jsp的脚本元素
概述:jsp的脚本元素就是在jsp中嵌入java代码
jsp的脚本元素的分类:
1.声明标签:
<%! 变量或方法声明 %>:写在这里的脚本中的代码,会被翻译成Servlet内部的成员变量或成员方法
2.表达式标签
<%= 表达式 %>:写在这个脚本中的代码,翻译成方法内部的out.print();当中跟的内容
3.程序代码标签
<% 程序代码 %>:写在这个脚本中的代码,翻译成方法内部的局部变量或方法内部代码片段
jsp的指令元素
指令元素的语法:<%@ 指令名称 属性名称=属性的值 属性名称=属性的值 %>
jsp指令元素的分类:
- page指令:指示jsp的页面设置属性和行为
- include指令:指示jsp包含哪些其他页面
- taglib指令:指示jsp页面包含哪些标签库
page指令:
写法:<%@page 属性名=属性值%>
作用:page指令用来定义jsp文件的全局属性
page指令属性:
- language属性:声明使用脚本的语言,现在只能是java
- extends属性:标明jsp编译成Servlet的时候继承的类,默认值是HttpJspBase
- session属性:标明jsp中是否可以直接使用session对象,默认值是true
- buffer属性:标明jsp对客户端输出缓冲区大小,默认是8kb
- autoFlush属性:如果缓冲区大小溢出,是否自动刷出,默认是true
- import属性:用于导入java包和类,只有这个属性可以出现多次,其他属性都只能一次
- contentType属性:标明jsp被浏览器解析和打开的时候采用的默认的字符集
- pageEncoding属性:jsp文件及jsp翻译后的servlet保存到硬盘上采用字符集
- isErrorPage属性:处理jsp页面异常,默认是false,设置是否允许在页面上显示错误信息
- errorPage属性:处理jsp页面异常,值就是出错是要跳转的页面
- isELIgnored属性:通知jsp是否忽略EL表达式,默认是false
include指令:
写法:<%@include 属性名=属性值%>
作用:在jsp页面中静态包含一个文件,同时由该jsp解析包含的文件内容
include指令属性:
- file属性:指示jsp页面包含的页面路径
静态包含的原理:实际上就是复制另外一个或者几个jsp文件的代码过来这边,然后翻译成一个Servlet
使用静态包含的注意地方:
- 应该将被包含的页面的结构去掉(不去掉就会带着head,body标签等一起复制过来,就会导致页面有重复的页面结构)
- 在被包含的页面中定义变量,在包含的页面中还可以使用(原因是静态包含其他页面的jsp文件会被翻译成一个Servlet)
Taglib指令
写法:<%@taglib 属性=属性值%>
作用:用于在jsp页面中引入标签库
Taglib指令属性:
- url属性:引入的标签库的路径
- prefix属性:引入的标签库的别名
3.jsp的内置对象
概述:jsp内置对象指的就是可以直接在jsp页面中使用的对象
jsp当中的9大内置对象
- request:从客户端向服务端发送请求的对象
- response:从服务器端向客户端做出响应的对象
- session:服务端为客户端创建会话对象
- application:代表应用,也就是获得的ServletContext对象
- out:向输出流写入内容的对象
- page:当前的jsp翻译成Servlet后的对象引用
- pageContext:本jsp的页面的上下文对象
- config:本jsp的ServletConfig对象
- exception:表示jsp页面运行时产生的异常对象
jsp的9大内置对象的具体类型
- request:HttpServletRequest
- response:HttpServletResponse
- session:HttpSession
- application:ServletContext
- out:JspWriter
- page:Object
- pageContext:PageContext
- config:ServletConfig
- exception:Throwable
pageContext对象
概述:pageContext对象直接翻译为“页面上下文”对象,代表的的是当前页面运行的一些属性
pageContext对象的使用:
1.提供了page范围的数据存取的方法
- setAttribute(String name, Object value)
- getAttribute(String name)
- removeAttribute(String name)
- findAttribute(String name):查找属性方法,先根据小范围的名称进行查找,如果找到了就返回,如果没有找到就会去比其大一个域的范围进行查找
2.通过这个对象获得其他8个内置对象
- getRequest()
- getResponse()
- getSession()
- getServletContext()
- getOut():这个比较特殊,是来自pageContext父类JspContext的方法
- getPage()
- getServletConfig()
- getException()
jsp的四个作用范围
1.PageScope:页面范围,指的是在当前页面内有效,出了这个页面,用pageContext保存的数据就无效了
2.RequestScope:请求范围,从客户端向服务端发送一次请求,服务器对这次请求做出了响应之后,用request保存的数据就无效了
3.SessionScope:会话范围,每个浏览器向服务器发送请求(多次请求),直到该会话结束)
4.ApplicationScope:应用范围,在整个应用中任意的地方都可以获取
通过pageContext来设置四个范围:
- pageContext.setAttribute(String name, Object value)
- pageContext.setAttribute(String name, Object value, PageContext.ReQuEST_SCOPE)
- pageContext.setAttribute(String name, Object value, PageCOntext.SESSION_SCOPE)
- pageContext.setAttribute(String name, Object value, PageContext.APPLICATION_SCOPE)
4.jsp的动作标签
概述:jsp的动作标签用于在jsp页面中提供业务逻辑功能,避免在jsp页面直接编写java代码,造成jsp页面难以维护
常用的动作标签
- jsp:forward标签:请求转发
- jsp:include标签:包含(动态包含)
- jsp:param标签:多个页面之间传递参数
动态包含的原理
在jsp程序执行时,完成包含操作,被包含的jsp会独立翻译执行,动态包含,包含的目标是jsp的运行结果
5.EL
概述:EL就是Expression Language(表达式语言),它的出现是为了使jsp写起来更加简单,表达式语言的灵感来自于ECMAScript和Xpath表达式语言,它提供了在jsp中简化表达式的方法,让jsp的代码更加简化
为什么要学习EL
EL和JSTL一起使用取代jsp页面中嵌入java代码的写法
EL的功能
- EL获取数据
- EL执行运算
- EL获取web开发常用的对象
- EL调用java方法
EL语法
${EL表达式}
EL如何获取数据
EL表达式语句在执行的时候,会调用pageContext.findAttribute()方法,分别从page、request、session、application范围查找相应对象,找到就会返回相应对象,找不到返回""(空的字符串),EL所获取的数据需要在这四个作用范围中
获取数组和集合中的数据:
- 获取数组的数据:通过中括号里面写下标即可获取,如$
- 获取集合中的数据:
- 获取List集合中的数据:也是通过中括号里面写下标即可获取,如$
- 获取Map集合中的数据:通过“.”获取Map集合的数据,如果Map的key中包含了特殊字符,则需要使用中括号里面写下标来获取,如${map.arg},$
EL执行运算
- 执行算数运算
- 执行关系运算
- 执行逻辑运算
- 执行三元运算
- 空运算
代码示例:
<h3>执行算数运算</h3>
<%
pageContext.setAttribute("n1", "10");
pageContext.setAttribute("n2", "20");
pageContext.setAttribute("n3", "40");
pageContext.setAttribute("n4", "30");
%>
${ n1+n2 }
<h3>执行关系运算符</h3>
${n1 > n2}--${n1 gt n2}<br>
${n1 < n2}--${n1 lt n2}<br>
${n1 == n2}--${n1 eq n2}<br>
${n1 >= n2}--${n1 le n2}<br>
${n1 <= n2}--${n1 ge n2}<br>
${n1 != n2}--${n1 ne n2}<br>
<h3>执行逻辑运算符</h3>
${n1 > n2 && n3 > n4}--${n1 > n2 and n3 > n4} <br>
${n1 > n2 || n3 > n4}--${n1 > n2 or n3 > n4} <br>
${! (n1 > n2)}--${not (n1 > n2)} <br>
<h3>三元运算符</h3>
${n1 > n2 ? "n1大于n2":"n1小于n2"}<br>
<h3>空运算符</h3>
${empty n1}<br>
EL获取web开发常用对象
- pageContext:相当于jsp内置对象中的pageContext
- pageScope:获取指定域下的名称和数据
- requestScope:获取指定域下的名称和数据
- sessionScope:获取指定域下的名称和数据
- applicationScope:获取指定域下的名称和数据
- param:在页面中接收请求参数(接收一个名称对应一个值的参数)
- paramValues:在页面中接收请求参数(接收一个名称对应多个值的参数)
- header:在页面上获取请求头(获取一个key对应一个value的头)
- headerValues:在页面上获取请求头(获取一个key对应多个value的头)
- cookie:访问cookie的名称和值,如${cookie.key.name},$
- initParam:获取全局初始化参数的值
6.JSTL
概述:JSTL(Java server pages standarded tag library,即JSP标准标签库)是由JCP(Java community Proces)所制定的标准规范,它主要提供给Java Web开发人员一个标准通用的标签库,并由Apache的Jakarta小组来维护。开发人员可以利用这些标签取代JSP页面上的Java代码,从而提高程序的可读性,降低程序的维护难度。
为什么学习JSTL
主要用于与EL来取代传统页面上直接嵌入java代码的写法,提升程序可读性、维护性和方便性
JSTL的标签库有哪些
- c标签(core标签库)
- fmt标签(国际化标签库)
- xml标签
- sql标签
- jstl函数库(EL函数)
JSTL的基本使用步骤
1.引入jar包
- standard.jar
- jstl.jar
2.新建jsp页面
3.引入标签库
- 通过<%@ taglib %>来引入
4.使用JSTL
7.监听器
概述:监听器就是一个实现了特定接口的java类,这个java类用于监听另一个java类的方法调用或者属性的改变,当被监听对象发上上述事件后,监听器某个方法就会被立即执行
监听器的用途:
用于监听其他对象的变化的,主要应用在图形界面开发上,如java中的GUI和安卓开发
监听器的术语:
- 事件源:指的是被监听对象
- 监听器:指的是负责监听的对象
- 事件源和监听器绑定
- 事件:指的是事件源对象的改变--主要功能是获得事件源对象
Servlet中的监听器
概述:在Servlet中定义了多种类型的监听器,它们用于监听的事件源分别是ServletContext,HttpSession,ServletRequest这三个对象
Servlet中的监听器的分类:
- 监听三个域对象的创建和销毁的监听器(三个)
- 监听三个域对象的属性变更(属性添加、移动、替换)的监听器(三个)
- 监听HttpSession中的JavaBean的状态改变(钝化、活化、绑定、解除绑定)的监听(两个)
ServletContextListener监听器
作用:用来监听ServletContext域对象的创建和销毁
ServletContext的创建与销毁:
- 在服务器启动的时候,为每个web应用创建单独的ServletContext对象
- 在服务器关闭的时候,或者项目从web服务器中移除的时候
使用实例:
//MyServletContextListener.java
package cn.luyi.ServletListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyServletContextListener implements ServletContextListener {
//监听ServletContext的销毁
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext对象被销毁了");
}
//监听ServletContext的创建
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext对象被创建了");
}
}
//web.xml关于listener的配置
<listener>
<listener-class>cn.luyi.ServletListener.MyServletContextListener</listener-class>
</listener>
HttpSessionListener监听器
作用:用来监听HttpSession对象的创建和销毁
HttpSession的创建和销毁:
- 服务器端第一次调用getSession()方法时创建
- 非正常关闭服务器时销毁(正常关闭服务器时session会被序列化)
- Session过期时也会销毁
- 手动调用session.invalidate()方法时销毁
代码实例:
//web.xml
<listener>
<listener-class>cn.luyi.ServletListener.MyHttpSessionListener</listener-class>
</listener>
//MyHttpSessionListener
package cn.luyi.ServletListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MyHttpSessionListener implements HttpSessionListener {
//监听HttpSession的创建
public void sessionCreated(HttpSessionEvent hse) {
System.out.println("HttpSession被创建");
}
//监听HttpSession的销毁
public void sessionDestroyed(HttpSessionEvent hse) {
System.out.println("HttpSession被销毁");
}
}
问题:
- 访问HTML文件是否创建session:不会
- 访问jsp是否创建session:会
- 访问servlet是否创建session:不会
ServletRequestListener监听器
作用:用户监听ServletRequest对象的创建和销毁
ServletRequest对象的创建和销毁
- 从客户端向服务器发送一次请求,服务器就会创建request对象
- 服务器对这次请求作出了响应之后,request对象就销毁了
代码示例:
//web.xml
<listener>
<listener-class>cn.luyi.ServletListener.MyServletRequestListener</listener-class>
</listener>
//MyServletRequestListener.java
package cn.luyi.ServletListener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class MyServletRequestListener implements ServletRequestListener {
//监听ServletRequest对象的销毁
public void requestDestroyed(ServletRequestEvent arg0) {
System.out.println("ServletRequest对象被销毁");
}
//监听ServletRequest对象的创建
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("ServletRequest对象被创建");
}
}
问题:
- 访问HTML页面是否会创建请求对象:会
- 访问jsp页面是否会创建请求对象:会
- 访问Servlet是否会创建请求对象:会
监听三个域对象的属性变更的监听器
- ServletContextAttributeListener:监听servletContext对象中的属性变更(属性添加、移除、替换)
- HttpSessionAttributeListener:监听HttpSession对象中的属性变更(属性添加、移除、替换)
- ServletRequestAttributeListener:监听ServletRequest对象中的属性变更(属性添加、移除、替换)
代码示例:
//web.xml
<listener>
<listener-class>cn.luyi.ServletListener.MyHttpSessionAttributeListener</listener-class>
</listener>
//MyHttpSessionAttributeListener.java
package cn.luyi.ServletListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
public class MyHttpSessionAttributeListener implements HttpSessionAttributeListener {
//监听HttpSession的属性是否被添加
public void attributeAdded(HttpSessionBindingEvent hse) {
System.out.println("向session中添加了属性");
}
////监听HttpSession的属性是否被移除
public void attributeRemoved(HttpSessionBindingEvent hse) {
System.out.println("从session中移除了属性");
}
//监听HttpSession的属性是否被替换
public void attributeReplaced(HttpSessionBindingEvent hse) {
System.out.println("向session中替换了属性");
}
}
监听HttpSession中java类状态改变的监听器
概述:保存在Session域中的java类可以有多种状态,包括绑定到session中,从session中解除绑定,随session对象持久化到一个存储设备中(钝化),随session对象从一个存储设备中恢复(活化),Servlet对方中定义了两个特殊的监听的接口来帮助java类了解自己在session域中的状态:
- HttpSessionBindingListener接口:监听HttpSession中java类与session的绑定与解除绑定
- HttpSessionActivationListener接口:监听HttpSession中java类中的钝化和活化监听器
实现这两个类不需要在web.xml中进行配置
HttpSessionBindingListener监听器的使用实例:
package cn.luyi.ServletListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
public class User implements HttpSessionBindingListener {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//监听javabean是否与session绑定
public void valueBound(HttpSessionBindingEvent arg0) {
System.out.println("User与session绑定了");
}
//监听javabean是否与session解除绑定
public void valueUnbound(HttpSessionBindingEvent arg0) {
System.out.println("User与session解除绑定了");
}
}
HttpSessionActivationListener监听器的使用示例:
代码实例:
package cn.luyi.ServletListener;
import java.io.Serializable;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionEvent;
public class Person implements HttpSessionActivationListener, Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//监听这个Prson类是否被活化了
public void sessionDidActivate(HttpSessionEvent arg0) {
System.out.println("user被活化(反序列化)了");
}
//监听这个Prson类是否被钝化了
public void sessionWillPassivate(HttpSessionEvent arg0) {
System.out.println("user被钝化(序列化)了");
}
}
补充:当服务器正常关闭时,保存在session里面的数据就会被钝化;当重新启动服务器时,session里面的数据就会被活化了
配置完成序列化与反序列化
配置的代码:
<Context>
<Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="1">
<Store className="org.apache.catalina.session.FileStore" directory="mysession"></Store>
</Manager>
</Context>
以上代码的配置可以配置在:
- tomcat/conf/context.xml:所有tomcat下虚拟主机和虚拟目录下的工程
- tomcat/conf/Catalina/localhost/context.xml:localhost虚拟主机下的所有项目会序列化
- 工程名/META-INF/context.xml:当前工程才会序列化session
8.Filter过滤器
概述:Filter称为过滤器,它是Servlet技术中最实用的技术,web开发人员通过Filter技术,对web服务器所管理的资源(jsp,servlet,静态图片html文件)进行拦截,从而实现一些特殊的功能。Filter就是过滤从客户端向服务器发送的请求
Filter的使用步骤
- 编写一个类实现Filter接口
- 对过滤器进行配置
代码示例:
web.xml对Filter的配置
<filter>
<filter-name>FilterDemo</filter-name>
<filter-class>com.luyi.filter.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
//FilterDemo.java
package com.luyi.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterDemo implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("FilterDemo执行了...");
//放行
chain.doFilter(request, response);
}
public void init(FilterConfig fConfig) throws ServletException {
}
}
FilterChain对象
概述:FilterChain过滤器链,在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称为一个过滤器链,web服务器根据Filter在web.xml文件中的注册顺序(mapping的配置顺序)决定先调用哪个Filter,依次调用后面的过滤器,如果没有下一个过滤器,调用目标资源
使用案例:
//web.xml配置三个过滤器
<filter>
<filter-name>FilterDemo</filter-name>
<filter-class>com.luyi.filter.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>FilterDemo2</filter-name>
<filter-class>com.luyi.filter.FilterDemo2</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>FilterDemo3</filter-name>
<filter-class>com.luyi.filter.FilterDemo3</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo3</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
//三个过滤器
//FilterDemo.java
package com.luyi.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterDemo implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("FilterDemo执行了...");
//放行
chain.doFilter(request, response);
System.out.println("FilterDemo执行结束了");
}
public void init(FilterConfig fConfig) throws ServletException {
}
}
//FilterDemo2.java
package com.luyi.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterDemo2 implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("FilterDemo2执行了");
chain.doFilter(request, response);
System.out.println("FilterDemo2执行结束了");
}
public void init(FilterConfig fConfig) throws ServletException {
}
}
//FilterDemo3.java
package com.luyi.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterDemo3 implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("FilterDemo3执行了");
chain.doFilter(request, response);
System.out.println("FilterDemo3执行结束了");
}
public void init(FilterConfig fConfig) throws ServletException {
}
}
//当访问当前工程下的资源时,就会向控制台输出:
FilterDemo执行了...
FilterDemo2执行了
FilterDemo3执行了
FilterDemo3执行结束了
FilterDemo2执行结束了
FilterDemo执行结束了
Filter的生命周期
Filter的创建和销毁是由web服务器负责,web应用程序启动的时候,web服务器创建Filter的实例对象,并调用其init方法进行初始化(filter对象只创建一次,init方法也只会执行一次)
每当filter进行拦截的时候,都会执行doFilter的方法
当服务器关闭的时候,应用从服务器中移除的时候,服务器就会销毁Filter对象
FilterConfig对象
作用:用来获取Filter的相关的配置的对象
FilterConfig的API
- String getFilterName():返回过滤器的名字
- String getInitParameter(String name):获取初始化参数的值
- Enumeration getInitParameterNames():获取所有的初始化参数名称
Filter的相关配置
1.url-pattern的配置:
- 完全路径匹配
- 目录匹配
- 扩展名匹配
2.servlet-name的配置(不必关注,因为通过拦截Servlet的请求路径也可以对其进行拦截)
- 专门以Servlet的配置的名称拦截servlet
3.dispatcher的配置
- REQUEST:默认值,默认过滤器拦截的就是请求
- FORWARD:拦截转发
- INCLUDE:页面包含的时候进行拦截
- ERROR:页面出现全局错误页面跳转的时候进行拦截
10.jspMVC模式开发案例(登录案例):
作者:卢一
出处:http://www.cnblogs.com/luyi001/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。