?
本文查阅方法:
??? 1、查阅目录 —— 查阅本文目录,确定想要查阅的目录标题
??? 2、快捷“查找” —— 在当前浏览器页面,按键 “Ctrl+F” 按键组合,开启浏览器的查找功能,
???????????? 在查找搜索框中 输入需要查阅的 目录标题,便可以直接到达 标题内容 的位置。
??? 3、学习小结 —— 文中的学习小结内容,是笔者在学习之后总结出的,开发时可直接参考其进行应用开发的内容, 进一步加快了本文的查阅 速度。(水平有限,仅供参考。)
?
?
?
?
?
?
本文目录
?
?学习小结
?
1、Filter技术?简介
?
2、Filter是如何实现拦截的?
?
3、Filter开发入门
?
4、Filter的生命周期
?
5、FilterConfig接口
?
6、Filter在开发中的常见应用的思想:
?
7、Filter常见应用(1)——统一全站字符编码的过滤器(解决中文乱码)
?
8、Filter常见应用(2)——控制浏览器缓存页面中的静态资源的过滤器
?
9、Filter常见应用(3)——禁止浏览器缓存所有动态页面的过滤器:
?
10、Filter常见应用(4)——使用Filter实现URL级别的权限认证
?
11、Filter常见应用(5)——实现用户自动登陆的过滤器
?
12、Filter的部署—注册Filter
?
13、Filter的部署—映射Filter
?
14、Filter映射——<dispatcher>?子元素可以设置的值及其意义
?
15、Filter高级开发
?
16、Decorator设计模式
?
17、Decorator设计模式的实现步骤
?
18、request对象的增强
?
19、request对象的增强案例
?
20、敏感词过滤器——过滤敏感词汇(禁用、审核、替换三种)
?
21、response对象的增强
?
22、response增强案例—压缩响应
?
23、实用案例-缓存数据到内存
?
?
?
????《Servlet?知识详解(一)之??——??ServletContext对象?和?ServletConfig对象?学习笔记》
?
????????地址:?http://even2012.iteye.com/blog/1838063
?
????《Servlet?知识详解(二)之??——??Request对象?和?Response对象?学习笔记》
?
????????地址:http://even2012.iteye.com/blog/1838099
?
????《Servlet?知识详解(三)之??——??Cookie对象?和?Session对象??学习笔记》
?
????????地址:http://even2012.iteye.com/blog/1838128
????《Servlet?知识详解(四)之??——? Filter对象 过滤器? 学习笔记》
?
????????地址:http://even2012.iteye.com/blog/1963466
????《Servlet?知识详解(五)之??——? Listener对象 监听器? 学习笔记》
?
????????地址:http://even2012.iteye.com/blog/1963467
?
?
?
学习小结
?
?
?
?
?
?
?
1、Filter技术 简介
?
????????Filter也称之为过滤器,它是Servlet技术中最激动人心的技术,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp,?Servlet,?静态图片文件或静态?html?文件等进行拦截,从而实现一些特殊的功能。例如实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。
?
????????Servlet?API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。通过Filter技术,开发人员可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,如下所示:
?
?
?
?
?
?
?
?
2、Filter是如何实现拦截的?
?
????????Filter接口中有一个doFilter方法,当开发人员编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的:
?
????????(1) 调用目标资源之前,让一段代码执行
?
????????(2) 是否调用目标资源(即是否让用户访问web资源)。
?
???????????????拦截原理:web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对象,它也提供了一个doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方法,即web资源就会被访问,否则web资源不会被访问。
?
????????(3) 调用目标资源之后,让一段代码执行
?
????????
?
?
?
?
3、Filter开发入门
?
????????Filter开发分为二个步骤:
?
????????????(1) 编写java类实现Filter接口,并实现其doFilter方法。
?
????????????(2) 在?web.xml?文件中使用<filter>和<filter-mapping>元素对编写的filter类进行注册,并设置它所能拦截的资源。
?
????????Filter链介绍
?
????????????(1) 在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称之为一个Filter链。
?
????????????(2) web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter,当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。
?
????????????(3) Filter链实验(查看FilterChain?API文档)
?
?
?
?
?
?
4、Filter的生命周期
?
????创建:init(FilterConfig?filterConfig)throws?ServletException:
?
????????(1) 和我们编写的Servlet程序一样,Filter的创建和销毁由WEB服务器负责。?web?应用程序启动时,web?服务器将创建Filter?的实例对象,并调用其init方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(注:filter对象只会创建一次,init方法也只会执行一次。)
?
????????(2) 开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
?
????销毁:destroy():
?
????????在Web容器卸载?Filter?对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。(Filter对象的销毁同Servlet一样:关闭Web容器或者卸载当前Web应用两种情况)
?
????????
?
?
?
?
5、FilterConfig接口
?
?????用户在配置filter时,可以使用<init-param>为filter配置一些初始化参数,当web容器实例化Filter对象,调用其init方法时,会把封装了filter初始化参数的filterConfig对象传递进来。因此开发人员在编写filter时,通过filterConfig对象的方法,就可获得:
?
????????(1) String?getFilterName():得到filter的名称。
?
????????(2) String?getInitParameter(String?name):?返回在部署描述中指定名称的初始化参数的值。如果不存在返回null.
?
????????(3) Enumeration?getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。
?
????????(4) public?ServletContext?getServletContext():返回Servlet上下文对象的引用。
?
????????
?
Demo样例:Filter文件 :?
?
public?class?FilterDemo1?implements?Filter?{
?
??private?FilterConfig?config;
?
?
?
??public?void?doFilter(ServletRequest?request,?ServletResponse?response,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????//从Filter配置信息的初始化参数中?取出设定的字符集参数的值。
?
????String?charEnc?=?this.config.getInitParameter("charEnc");
?
?
?
????//将取出的字符集用于设定?全站的字符编码设定。
?
????request.setCharacterEncoding(charEnc);
?
????response.setCharacterEncoding(charEnc);
?
????response.setContentType("text/html;charset="+charEnc);
?
?
?
????System.out.println("servletDemo1之前");????
?
????chain.doFilter(request,?response);??//放行????
?
????System.out.println("servletDemo1之后!!");
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????System.out.println("filter被创建了!!");
?
????this.config?=?filterConfig;
?
??}
?
?
?
??public?void?destroy()?{
?
????System.out.println("filter被销毁了!!");
?
??}
?
}
?
?
?
web.xml文件
?
<filter>
?
????<filter-name>FilterDemo1</filter-name>
?
????<filter-class>cn.itcast.web.filter.FilterDemo1</filter-class>
?
????<init-param>
?
??????<param-name>charEnc</param-name>
?
??????<param-value>UTF-8</param-value>
?
????</init-param>
?
??</filter>
?
<filter-mapping>
?
????<filter-name>FilterDemo1</filter-name>
?
????<url-pattern>/*</url-pattern>
?
??</filter-mapping>
?
?
?
?
?
?
6、Filter在开发中的常见应用的思想:
?
?????(1) filter可以目标资源执行之前,进行权限检查,检查用户有无权限,如有权限则放行,如没有,则拒绝访问
?
?????(2) filter可以放行之前,对request和response进行预处理,从而实现一些全局性的设置。
?
?????(3) filter在放行之后,可以捕获到目标资源的输出,从而对输出作出类似于压缩这样的设置
?
?
?
?
?
?
7、Filter常见应用(1)——统一全站字符编码的过滤器(解决中文乱码)
?
????通过配置参数encoding指明使用何种字符编码,以处理Html?Form请求参数的中文问题
?
Demo样例1:Filter类?
?
//真正解决全站乱码(Post?和?Get?方式?全部解决)
?
public?class?CharacterEncodingFilter2?implements?Filter?{??
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????request.setCharacterEncoding("UTF-8");??//post??get
?
????response.setCharacterEncoding("UTF-8");
?
????response.setContentType("text/html;charset=UTF-8");
?
?
?
????chain.doFilter(new?MyRequest(request),?response);???//request.getparameter("password");
?
??}
?
?
?
??class?MyRequest?extends?HttpServletRequestWrapper{
?
????private?HttpServletRequest?request;
?
????public?MyRequest(HttpServletRequest?request)?{
?
??????super(request);
?
??????this.request?=?request;
?
????}
?
????@Override
?
????public?String?getParameter(String?name)?{??????
?
??????String?value?=?this.request.getParameter(name);
?
??????if(!request.getMethod().equalsIgnoreCase("get")){
?
????????return?value;
?
??????}
?
?
?
??????if(value==null){
?
????????return?null;
?
??????}
?
?
?
??????try?{
?
????????return?value?=?new?String(value.getBytes("iso8859-1"),request.getCharacterEncoding());
?
??????}?catch?(UnsupportedEncodingException?e)?{
?
????????throw?new?RuntimeException(e);
?
??????}??????
?
????}????
?
??}
?
?
?
??public?void?destroy()?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
??}
?
}
?
?
?
Demo样例2:web.xml文件
?
<filter>
?
????<filter-name>CharacterEncodingFilter2</filter-name>
?
????<filter-class>cn.itcast.web.filter.example.CharacterEncodingFilter2</filter-class>
?
??</filter>
?
?
?
??<filter-mapping>
?
????<filter-name>CharacterEncodingFilter2</filter-name>
?
????<url-pattern>/*</url-pattern>
?
??</filter-mapping>
?
?
?
?
?
?
?
?
8、Filter常见应用(2)——控制浏览器缓存页面中的静态资源的过滤器
?
场景:有些动态页面中引用了一些图片或css文件以修饰页面效果,这些图片和css文件经常是不变化的,所以为减轻服务器的压力,可以使用filter控制浏览器缓存这些文件,以提升服务器的性能。
?
?
?
Demo样例1:Filter类?:?
?
//控制浏览器缓存的过滤器
?
public?class?CacheFilter?implements?Filter?{
?
??private?FilterConfig?config;
?
?
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????//1.获取到用户想访问的资源
?
????String?uri?=?request.getRequestURI();
?
?
?
????//2.获取该资源的缓存时间
?
????int?expires?=?0;
?
????if(uri.endsWith(".jpg")){
?
??????expires?=?Integer.parseInt(this.config.getInitParameter("jpg"));
?
????}else?if(uri.endsWith(".css")){
?
??????expires?=?Integer.parseInt(this.config.getInitParameter("css"));
?
????}else{
?
??????expires?=?Integer.parseInt(this.config.getInitParameter("js"));
?
????}
?
?
?
????response.setDateHeader("expires",?System.currentTimeMillis()+expires*60*1000);
?
????chain.doFilter(request,?response);
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????this.config?=?filterConfig;
?
??}
?
?
?
??public?void?destroy()?{
?
?
?
??}
?
}
?
?
?
Demo样例2:web.xml文件?:
?
<filter>
?
????<filter-name>CacheFilter</filter-name>
?
????<filter-class>cn.itcast.web.filter.example.CacheFilter</filter-class>
?
????<init-param>
?
??????<param-name>css</param-name>
?
??????<param-value>10</param-value>
?
????</init-param>
?
????<init-param>
?
??????<param-name>jpg</param-name>
?
??????<param-value>1</param-value>
?
????</init-param>
?
????<init-param>
?
??????<param-name>js</param-name>
?
??????<param-value>20</param-value>
?
????</init-param>
?
??</filter>?
?
?
?
??<filter-mapping>
?
????<filter-name>CacheFilter</filter-name>
?
????<url-pattern>*.jpg</url-pattern>
?
??</filter-mapping>
?
?
?
????<filter-mapping>
?
????<filter-name>CacheFilter</filter-name>
?
????<url-pattern>*.css</url-pattern>
?
??</filter-mapping>
?
?
?
???<filter-mapping>
?
????<filter-name>CacheFilter</filter-name>
?
????<url-pattern>*.js</url-pattern>
?
??</filter-mapping>
?
??
?
?
??
?
9、Filter常见应用(3)——禁止浏览器缓存所有动态页面的过滤器:
?
????(1) 有?3?个?HTTP?响应头字段都可以禁止浏览器缓存当前页面,它们在?Servlet?中的示例代码如下:
?
????????(a) response.setDateHeader("Expires",-1);
?
????????(b) response.setHeader("Cache-Control","no-cache");?
?
????????(c) response.setHeader("Pragma","no-cache");?
?
????(2) 并不是所有的浏览器都能完全支持上面的三个响应头,因此最好是同时使用上面的三个响应头。
?
????(3) Expires数据头:值为GMT时间值,为-1指浏览器不要缓存页面
?
????(4) Cache-Control响应头有两个常用值:?
?
????????(a) no-cache指浏览器不要缓存当前页面。
?
??????? (b)?max-age:xxx指浏览器缓存页面xxx秒。
?
【备注:此例较为简单,Demo样例只要按照上例《8、Filter常见应用(2)——控制浏览器缓存页面中的静态资源的过滤器 》 即可实现。】
?
Demo样例1:Filter类?
?
?????????参见《8、Filter常见应用(2)——控制浏览器缓存页面中的静态资源的过滤器 》
?
Demo样例2:web.xml文件?
?
?????????参见《8、Filter常见应用(2)——控制浏览器缓存页面中的静态资源的过滤器 》
?
Demo样例3:Service类???
?
?????????参见《8、Filter常见应用(2)——控制浏览器缓存页面中的静态资源的过滤器 》
?
?
?
?
??
?
10、Filter常见应用(4)——使用Filter实现URL级别的权限认证
?
情景:在实际开发中我们经常把一些执行敏感操作的servlet映射到一些特殊目录中,并用filter把这些特殊目录保护起来,限制只能拥有相应访问权限的用户才能访问这些目录下的资源。从而在我们系统中实现一种URL级别的权限功能。
?
要求:为使Filter具有通用性,Filter保护的资源和相应的访问权限通过filter参数的形式予以配置。
?
?
?
Demo样例1:Filter类?
?
?
?
Demo样例2:web.xml文件
?
?
?
Demo样例3:Service类??
?
?
?
?
?
?
?
??
?
11、Filter常见应用(5)——实现用户自动登陆的过滤器
?
???????(1)?在用户登陆成功后,发送一个名称为user的cookie给客户端,cookie的值为用户名和md5加密后的密码。
?
???????(2) 编写一个AutoLoginFilter,这个filter检查用户是否带有名称为user的cookie来,如果有,则调用dao查询cookie的用户名和密码是否和数据库匹配,匹配则向session中存入user对象(即用户登陆标记),以实现程序完成自动登陆。
?
?????????
?
Demo样例1:Filter类??
?
public?class?AutoLoginFilter?implements?Filter?{??
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????//1.先检查用户是否已登陆,没登陆才自动登陆
?
????User?user?=?(User)?request.getSession().getAttribute("user");
?
????if(user!=null){
?
??????chain.doFilter(request,?response);
?
??????return;
?
????}
?
?
?
????//2.没登陆,再执行自动登陆逻辑
?
?
?
????//(1)看用户有没有带自动登陆的cookie
?
????Cookie?autoLoginCookie?=?null;
?
????Cookie?cookies[]?=?request.getCookies();
?
????for(int?i=0;cookies!=null?&&?i<cookies.length;i++){
?
??????if(cookies[i].getName().equals("autologin")){
?
????????autoLoginCookie?=?cookies[i];
?
??????}
?
????}
?
????if(autoLoginCookie==null){
?
??????chain.doFilter(request,?response);
?
??????return;
?
????}
?
?
?
????//(2)用户带了自动登陆的cookie,则先检查cookie的有效期?
?
????String?values[]?=?autoLoginCookie.getValue().split("\\:");
?
????if(values.length!=3){
?
??????chain.doFilter(request,?response);
?
??????return;
?
????}
?
????long?expirestime?=?Long.parseLong(values[1]);
?
????if(System.currentTimeMillis()>expirestime){
?
??????chain.doFilter(request,?response);
?
??????return;
?
????}
?
?
?
????//(3)代表cookie时间有效,再检查cookie的有效性
?
????String?username?=?values[0];
?
????String?client_md5?=?values[2];
?
?
?
????BusinessService?service?=?new?BusinessService();
?
????user?=?service.findUser(username);
?
????if(user==null){
?
??????chain.doFilter(request,?response);
?
??????return;
?
????}
?
????//发挥浏览器Cookie的格式:autologin=username:expirestime:md5(password:expirestime:username)
?
????String?server_md5?=?md5(user.getUsername(),user.getPassword(),expirestime);
?
????if(!server_md5.equals(client_md5)){
?
??????chain.doFilter(request,?response);
?
??????return;
?
????}
?
?
?
????//(4)执行登陆
?
????request.getSession().setAttribute("user",?user);
?
????chain.doFilter(request,?response);????
?
??}
?
?
?
??//MD5加密算法
?
??private?String?md5(String?username,String?password,long?expirestime){????
?
????try{
?
??????String?value?=?password?+?":"?+?expirestime?+?":"?+?username;
?
??????MessageDigest?md?=?MessageDigest.getInstance("md5");
?
??????byte?md5[]?=?md.digest(value.getBytes());
?
??????BASE64Encoder?encode?=?new?BASE64Encoder();
?
??????return?encode.encode(md5);
?
????}catch?(Exception?e)?{
?
??????throw?new?RuntimeException(e);
?
????}
?
??}??
?
?
?
??public?void?destroy()?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
}
?
?
?
Demo样例2:web.xml文件?
?
??<filter>
?
????<filter-name>AutoLoginFilter</filter-name>
?
????<filter-class>cn.itcast.web.filter.example.AutoLoginFilter</filter-class>
?
??</filter>??
?
?
?
??<filter-mapping>
?
????<filter-name>AutoLoginFilter</filter-name>
?
????<url-pattern>/*</url-pattern>
?
??</filter-mapping>
?
?
?
Demo样例3:Servlet类??
?
public?class?LoginServlet?extends?HttpServlet?{?
?
??public?void?doGet(HttpServletRequest?request,?HttpServletResponse?response)
?
??????throws?ServletException,?IOException?{
?
?
?
????String?username?=?request.getParameter("username");
?
????String?password?=?request.getParameter("password");
?
?
?
????BusinessService?service?=?new?BusinessService();
?
????User?user?=?service.login(username,?password);
?
????if(user==null){
?
??????request.setAttribute("message",?"用户名或密码错误!!");
?
??????request.getRequestDispatcher("/message.jsp").forward(request,?response);
?
??????return;
?
????}
?
?
?
????request.getSession().setAttribute("user",?user);
?
????int?expirestime?=?Integer.parseInt(request.getParameter("time"));
?
????//给客户机发送自动登陆的?cookie??
?
????//autologin=username:expirestime:md5(password:expirestime:username)
?
????Cookie?cookie?=?makeCookie(user,?expirestime);
?
????response.addCookie(cookie);
?
????response.sendRedirect("/day19/index.jsp");
?
??}
?
?
?
??//生成发回给浏览器的Cookie
?
??public?Cookie?makeCookie(User?user,int?expirestime){
?
????long?currenttime?=?System.currentTimeMillis();
?
????String?cookieValue?=?user.getUsername()?+?":"?+?(currenttime+expirestime*1000)?+?":"?+?md5(user.getUsername(),?user.getPassword(),?(currenttime+expirestime*1000));
?
????Cookie?cookie?=?new?Cookie("autologin",cookieValue);
?
????cookie.setMaxAge(expirestime);
?
????cookie.setPath("/day19");
?
????return?cookie;
?
??}
?
?
?
??//MD5?加密算法
?
??private?String?md5(String?username,String?password,long?expirestime){
?
?
?
????try{
?
??????String?value?=?password?+?":"?+?expirestime?+?":"?+?username;
?
??????MessageDigest?md?=?MessageDigest.getInstance("md5");
?
??????byte?md5[]?=?md.digest(value.getBytes());
?
??????BASE64Encoder?encode?=?new?BASE64Encoder();
?
??????return?encode.encode(md5);
?
????}catch?(Exception?e)?{
?
??????throw?new?RuntimeException(e);
?
????}
?
??}
?
?
?
??public?void?doPost(HttpServletRequest?request,?HttpServletResponse?response)
?
??????throws?ServletException,?IOException?{
?
????doGet(request,?response);
?
??}
?
}
?
?
?
Demo样例4:Service类?
?
public?class?BusinessService?{?
?
??private?static?List<User>?list?=?new?ArrayList();
?
??static{
?
????list.add(new?User("aaa","123"));
?
????list.add(new?User("bbb","123"));
?
????list.add(new?User("ccc","123"));
?
??}
?
?
?
??public?User?login(String?username,String?password){
?
????for(User?user?:?list){
?
??????if(user.getUsername().equals(username)?&&?user.getPassword().equals(password)){
?
????????return?user;
?
??????}
?
????}
?
????return?null;
?
??}
?
?
?
??public?User?findUser(String?username){
?
????for(User?user?:?list){
?
??????if(user.getUsername().equals(username)){
?
????????return?user;
?
??????}
?
????}
?
????return?null;
?
??}??
?
}
?
?
?
Demo样例5:User类??
?
?
?
?
?
?
?
?
12、Filter的部署—注册Filter
?
Demo样例:
?
<filter>
?
????????<filter-name>testFitler</filter-name>
?
???????<filter-class>org.test.TestFiter</filter-class>
?
???????<init-param>
?
?????????????<param-name>word_file</param-name>??
?
?????????????<param-value>/WEB-INF/word.txt</param-value>
?
???????</init-param>
?
</filter>
?
?详解:
?
????????(1) <filter-name>用于为过滤器指定一个名字,该元素的内容不能为空。
?
????????(2) <filter-class>元素用于指定过滤器的完整的限定类名。
?
????????(3) <init-param>元素用于为过滤器指定初始化参数,它的子元素<param-name>指定参数的名字,<param-value>指定参数的值。在过滤器中,可以使用FilterConfig接口对象来访问初始化参数。
?
?
?
?
??
?
13、Filter的部署—映射Filter
?
????<filter-mapping>元素用于设置一个?Filter?所负责拦截的资源。一个Filter拦截的资源可通过两种方式来指定:Servlet?名称和资源访问的请求路径
?
????????(1) <filter-name>子元素用于设置filter的注册名称。该值必须是在<filter>元素中声明过的过滤器<filter-name> 的名字
?
????????(2) <url-pattern>设置?filter?所拦截的请求路径(过滤器关联的URL样式:规则同Servlet中的相同)
?
????????(3) <servlet-name>指定过滤器所拦截的Servlet名称。(比如:缓存分类信息时,拦截查询数据库分类信息的servlet)
?
????????(4) <dispatcher>指定过滤器所拦截的资源被?Servlet?容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个<dispatcher>?子元素用来指定?Filter?对资源的多种调用方式进行拦截。
?
?
?
????Filter的部署—映射Filter示例
?
????<filter-mapping>
?
?????????<filter-name>testFilter</filter-name>
?
????????<url-pattern>/test.jsp</url-pattern>
?
????</filter-mapping>?
?
?????
?
????<filter-mapping>
?
????????<filter-name>testFilter</filter-name>
?
???????<url-pattern>/index.jsp</url-pattern>
?
???????<dispatcher>REQUEST</dispatcher>
?
???????<dispatcher>FORWARD</dispatcher>
?
????</filter-mapping>
?
?
?
?
?
?
?
?
?
14、Filter映射——<dispatcher>?子元素可以设置的值及其意义
?
????????(1) REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
?
????????(2) INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
?
????????(3) FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
?
????????(4) ERROR:如果目标资源是通过“声明式异常处理机制”调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。(Jsp页面的错误页面设定机制)?????????
?
【备注:其实四种调用方式的每种指令都是由Servlet先反馈给Web服务器,再由Web服务器根据反馈指令所要求的方式去调用指定的类。】
?
?
?
?
??
?
?
?
?
15、Filter高级开发
?
????????由于开发人员在filter中可以得到代表用户请求和响应的request、response对象,因此在编程中可以使用Decorator(装饰器)模式对request、response对象进行包装,再把包装对象传给目标资源,从而实现一些特殊需求。
?
?
?
?
?
?
16、Decorator设计模式
?
????(1) 当某个对象的方法不适应业务需求时,通常有2种方式可以对方法进行增强:
?
????????????(a) 编写子类,覆盖需增强的方法
?
????????????(b) 使用Decorator设计模式对方法进行增强
?
????(2) 疑问:在实际应用中遇到需增强对象的方法时,到底选用哪种方式呢?
?
????????????(a) 没有具体的定式,不过有一种情况下,必须使用Decorator设计模式:即被增强的对象,开发人员只能得到它的对象,无法得到它的class文件。
?
????????????(b) 比如request、response对象,开发人员之所以在servlet中能通过sun公司定义的HttpServletRequest\response接口去操作这些对象,是因为Tomcat服务器厂商编写了request、response接口的实现类。web服务器在调用servlet时,会用这些接口的实现类创建出对象,然后传递给servlet程序。
?
????????????(c) 此种情况下,由于开发人员根本不知道服务器厂商编写的request、response接口的实现类是哪个?在程序中只能拿到服务器厂商提供的对象,因此就只能采用Decorator设计模式对这些对象进行增强。
?
?
?
?
?
?
17、Decorator设计模式的实现步骤
?
????????(1) 首先看需要被增强对象继承了什么接口或父类,编写一个类也去继承这些接口或父类。
?
????????(2) 在类中定义一个变量,变量类型即需增强对象的类型。
?
????????(3) 在类中定义一个构造函数,接收需增强的对象。
?
????????(4) 覆盖需增强的方法,编写增强的代码。
?
????????(5) 实现接口中其他的方式,并用被增强对象调用相同方法以返回结果。
?
?
?
?
?
?
18、request对象的增强
?
????????Servlet?API?中提供了一个request对象的Decorator设计模式的默认实现类HttpServletRequestWrapper?,?(HttpServletRequestWrapper?类实现了request?接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的?request?对象的对应方法)以避免用户在对request对象进行增强时需要实现request接口中的所有方法。
?
?
?
?
?
?
19、request对象的增强案例
?
????????(1) 使用Decorator模式包装request对象,完全解决get、post请求方式下的中文乱码问题。?(Demo参见标题7、Filter常见应用(1)——统一全站字符编码的过滤器(解决中文乱码) 中的Demo样例)
?
????????(2) 使用Decorator模式包装request对象,实现html标签转义功能(Tomcat服务器中提供了转义html标签的工具类)。
?
【Html转义:方式浏览器表单被恶意提交脚本代码,当被再次使用浏览器浏览时,就是执行这些脚本代码从而对用户造成?损害。如意:死循环弹出确认对话框】
?
?
?
Demo样例1:Filter类??
?
public?class?HtmlFilter?implements?Filter?{
?
??public?void?destroy()?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
?
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????chain.doFilter(new?MyRequest(request),?response);??//request.getParameter("resume");??//<script>
?
??}
?
?
?
??装饰模式?增强?request的功能——实现Html转义功能。
?
??class?MyRequest?extends?HttpServletRequestWrapper{
?
????private?HttpServletRequest?request;
?
????public?MyRequest(HttpServletRequest?request)?{
?
??????super(request);
?
??????this.request?=?request;
?
????}
?
????@Override
?
????public?String?getParameter(String?name)?{
?
?
?
??????String?value?=?this.request.getParameter(name);
?
??????if(value==null){
?
????????return?null;
?
??????}
?
??????return?filter(value);
?
????}
?
?
?
????//执行Html转义的方法
?
????public?String?filter(String?message)?{
?
?
?
??????????if?(message?==?null)
?
??????????????return?(null);
?
?
?
??????????char?content[]?=?new?char[message.length()];
?
??????????message.getChars(0,?message.length(),?content,?0);
?
??????????StringBuffer?result?=?new?StringBuffer(content.length?+?50);
?
??????????for?(int?i?=?0;?i?<?content.length;?i++)?{
?
??????????????switch?(content[i])?{
?
??????????????case?'<':
?
??????????????????result.append("<");
?
??????????????????break;
?
??????????????case?'>':
?
??????????????????result.append(">");
?
??????????????????break;
?
??????????????case?'&':
?
??????????????????result.append("&");
?
??????????????????break;
?
??????????????case?'"':
?
??????????????????result.append(""");
?
??????????????????break;
?
??????????????default:
?
??????????????????result.append(content[i]);
?
??????????????}
?
??????????}
?
??????????return?(result.toString());
?
??????}????
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
}
?
?
?
Demo样例2:web.xml文件
?
<filter>
?
????<filter-name>HtmlFilter</filter-name>
?
????<filter-class>cn.itcast.web.filter.example.HtmlFilter</filter-class>
?
??</filter>?
?
?
?
??<filter-mapping>
?
????<filter-name>HtmlFilter</filter-name>
?
????<url-pattern>/*</url-pattern>
?
??</filter-mapping>
?
?
?
?
?
?
?
?20、敏感词过滤器——过滤敏感词汇(禁用、审核、替换三种)
?
实际开发中我们需要对用户提交的言论,进行敏感词的过滤,否则会触动监管部门。
?
敏感词汇 可分为禁用、审核、替换三种
?
思路:
?
1、读取词库——将敏感词汇库文件分类读取到这三种词汇集合中(三个List集合)
?
2、匹配——使用正则表达式,将三个集合中的每个词汇都取出来到用户提交的内容中进行匹配,
?
????????(1)若是有禁用词汇,直接告诉用户含有敏感词,请修改后再提交,不存入数据库;
?
????????(2)若是有审核词汇,则将审核词汇进行高亮修饰后,再存入数据库,由管理员进行查阅审核。
?
????????(3)若是有替代词,则直接使用预定的符号或者词汇,进行替换,然后存入数据库。
?
??
?
Demo样例:Filter.java过滤器内容
?
public?class?WordsFilter?implements?Filter?{
?
?
?
??private?List<String>?banWords?=?new?ArrayList();
?
??private?List<String>?auditWords?=?new?ArrayList();
?
??private?List<String>?replaceWords?=?new?ArrayList();
?
?
?
??//在过滤器初始化的时候就将词汇库读入到三个集合中。
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????try{
?
??????String?path?=?WordsFilter.class.getClassLoader().getResource("cn/itcast/words").getPath();
?
??????File?files[]?=??new?File(path).listFiles();
?
??????for(File?file?:?files){
?
????????if(!file.getName().endsWith(".txt")){
?
??????????continue;
?
????????}
?
????????BufferedReader?br?=?new?BufferedReader(new?FileReader(file));
?
????????String?line?=?null;
?
????????while((line=br.readLine())!=null){
?
??????????String?s[]?=?line.split("\\|");
?
??????????if(s.length!=2){
?
????????????continue;
?
??????????}
?
??????????if(s[1].trim().equals("1")){
?
????????????banWords.add(s[0].trim());
?
??????????}
?
?
?
??????????if(s[1].trim().equals("2")){
?
????????????auditWords.add(s[0].trim());
?
??????????}
?
?
?
??????????if(s[1].trim().equals("3")){
?
????????????replaceWords.add(s[0].trim());
?
??????????}
?
????????}
?
??????}
?
??????System.out.println("haha");
?
????}catch?(Exception?e)?{
?
??????throw?new?RuntimeException(e);
?
????}
?
??}
?
?
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????//检查提交数据是否包含禁用词,若含有则直接返回到浏览器,不存入数据库,不放行。
?
????Enumeration?e?=?request.getParameterNames();
?
????while(e.hasMoreElements()){
?
??????String?name?=?(String)?e.nextElement();
?
??????String?data?=?request.getParameter(name);?
?
??????for(String?regex?:?banWords){
?
????????Pattern?pattern?=?Pattern.compile(regex);
?
????????Matcher?m?=?pattern.matcher(data);
?
????????if(m.find()){
?
??????????request.setAttribute("message",?"文章中包括非法词汇,请检查后再提交!!");
?
??????????request.getRequestDispatcher("/message.jsp").forward(request,?response);
?
??????????return;
?
????????}
?
??????}
?
????}
?
?
?
????//检查审核词?和?检查替换词?需要放行?以便存入到数据库中,只是在存入到数据库前,需要对数据进行敏感词处理(高亮或者替换)。
?
????chain.doFilter(new?MyRequest(request),?response);?
?
??}??
?
?
?
??class?MyRequest?extends?HttpServletRequestWrapper{
?
????private?HttpServletRequest?request;
?
????public?MyRequest(HttpServletRequest?request)?{
?
??????super(request);
?
??????this.request?=?request;
?
????}
?
????@Override
?
????public?String?getParameter(String?name)?{
?
?
?
??????String?data?=?this.request.getParameter(name);
?
??????if(data==null){
?
????????return?null;
?
??????}
?
??????//检查审核词
?
??????for(String?regex?:?auditWords){
?
????????Pattern?p?=?Pattern.compile(regex);
?
????????Matcher?m?=?p.matcher(data);
?
????????if(m.find()){????//我有一把仿真汽车,你要电鸡吗??
?
??????????String?value?=?m.group();??//找出客户机提交的数据中和正则表达式相匹配的数据
?
??????????data?=?data.replaceAll(regex,?"<font?color='red'>"?+?value?+?"</font>");
?
????????}
?
??????}
?
?
?
??????//检查替换词
?
??????for(String?regex?:?replaceWords){
?
????????Pattern?p?=?Pattern.compile(regex);
?
????????Matcher?m?=?p.matcher(data);
?
????????if(m.find()){????//我有一把仿真汽车,你要电鸡吗??
?
??????????data?=?data.replaceAll(regex,?"*******");
?
????????}
?
??????}
?
?
?
??????return?data;
?
????}
?
??}
?
?
?
??public?void?destroy()?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
}
?
??
?
?
?
?web.xml文件
?
?
?
?
?
?
?
?
?
?
21、response对象的增强
?
????????Servlet??API?中提供了response对象的Decorator设计模式的默认实现类HttpServletResponseWrapper?,?(HttpServletResponseWrapper类实现了response接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的?response对象的对应方法)以避免用户在对response对象进行增强时需要实现response接口中的所有方法。
?
?
?
?
?
?
22、response增强案例—压缩响应
?
应用HttpServletResponseWrapper对象,压缩响应正文内容。思路:
?
????????(1) 通过filter向目标页面传递一个自定义的response对象。
?
????????????????(a) 在自定义的response对象中,重写getOutputStream方法和getWriter方法,使目标资源调用此方法输出页面内容时,获得的是我们自定义的ServletOutputStream对象。
?
????????????????(b) 在我们自定义的ServletOuputStream对象中,重写write方法,使写出的数据写出到一个buffer中。
?
????????(2) 当页面完成输出后,在filter中就可得到页面写出的数据,从而我们可以调用GzipOuputStream对数据进行压缩后再写出给浏览器,以此完成响应正文件压缩功能。
?
?
?
Demo样例1:Filter类??
?
public?class?GzipFilter?implements?Filter?{?
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????MyResponse?myresponse?=?new?MyResponse(response);????
?
?
?
????chain.doFilter(request,?myresponse);??//response.getwriter??response.getOutputStream??
?
?
?
????//取出缓冲的数据压缩后输出
?
????byte?out[]?=?myresponse.getBuffer();??//得到目标资源的输出
?
????System.out.println("压之前:"?+?out.length);????
?
?
?
????byte?gzipout[]?=?gzip(out);
?
????System.out.println("压之后:"?+?gzipout.length);
?
????//备注:GZIPOutputStream?的压缩方式,只是对?文本文件(如:jsp、html、css、js)?压缩率很高,但是对于其他的二进制文件资源(如:视频、图片、音频)的压缩率非常一般,而且当二进制文件很大时(如视频文件很大),就会造成数据流?缓冲区的?溢出。所以该过滤器?应该仅?过滤压缩?文本文件,而避免过滤压缩二进制文件。
?
?
?
????response.setHeader("content-encoding",?"gzip");
?
????response.setHeader("content-length",?gzipout.length?+?"");
?
????response.getOutputStream().write(gzipout);
?
??}
?
?
?
??//?执行压缩功能的?GZIPOutputStream
?
??public?byte[]?gzip(byte?b[])?throws?IOException{????
?
????ByteArrayOutputStream?bout?=?new?ByteArrayOutputStream();
?
????GZIPOutputStream?gout?=?new?GZIPOutputStream(bout);
?
????gout.write(b);
?
????gout.close();
?
????return?bout.toByteArray();
?
??}
?
?
?
??//采用Decorator装饰器的?自定义?Response类
?
??class?MyResponse?extends?HttpServletResponseWrapper{
?
????private?ByteArrayOutputStream?bout?=?new?ByteArrayOutputStream();
?
????private?PrintWriter?pw;
?
?
?
????private?HttpServletResponse?response;
?
????public?MyResponse(HttpServletResponse?response)?{
?
??????super(response);
?
??????this.response?=?response;
?
????}
?
????@Override
?
????public?ServletOutputStream?getOutputStream()?throws?IOException?{
?
??????return?new?MyServletOutputStream(bout);????//myresponse.getOutputStream().write("hahah");
?
????}
?
?
?
????@Override
?
????public?PrintWriter?getWriter()?throws?IOException?{
?
??????pw?=?new?PrintWriter(new?OutputStreamWriter(bout,response.getCharacterEncoding()));
?
??????return?pw;??//MyResponse.getWriter().write("中国");
?
????}
?
????public?byte[]?getBuffer(){
?
??????if(pw!=null){
?
????????pw.close();
?
??????}
?
??????return?bout.toByteArray();
?
????}
?
??}
?
?
?
??//自定义ServletOutputStream?类,用于返回给Servlet层调用
?
??class?MyServletOutputStream?extends?ServletOutputStream{
?
?
?
????private?ByteArrayOutputStream?bout;
?
????public?MyServletOutputStream(ByteArrayOutputStream?bout){
?
??????this.bout?=?bout;
?
????}
?
????@Override
?
????public?void?write(int?b)?throws?IOException?{
?
??????bout.write(b);
?
????}
?
?
?
??}
?
?
?
??public?void?destroy()?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????//?TODO?Auto-generated?method?stub
?
??}?
?
}
?
?
?
Demo样例2:web.xml文件??
?
??<filter>
?
????<filter-name>GzipFilter</filter-name>
?
????<filter-class>cn.itcast.web.filter.example.GzipFilter</filter-class>
?
??</filter>
?
??<!--备注1:GZIPOutputStream?的压缩方式,只是对?文本文件(如:jsp、html、css、js)?压缩率很高,但是对于其他的二进制文件资源(如:视频、图片、音频)的压缩率非常一般,而且当二进制文件很大时(如视频文件很大),就会造成数据流?缓冲区的?溢出。所以该过滤器?应该仅?过滤压缩?文本文件,而避免过滤压缩二进制文件。-->
?
??<!--备注2:?我们很多时候?调用jsp文件都是通过转发的方式?调用(WEB-INF文件夹下面外界无法访问),所以一定要设定<dispatcher>FORWARD</dispatcher>?-->
?
??<filter-mapping>
?
????<filter-name>GzipFilter</filter-name>
?
????<url-pattern>*.jsp</url-pattern>
?
????<dispatcher>FORWARD</dispatcher>
?
????<dispatcher>REQUEST</dispatcher>
?
??</filter-mapping>?
?
?
?
??<filter-mapping>
?
????<filter-name>GzipFilter</filter-name>
?
????<url-pattern>*.html</url-pattern>
?
??</filter-mapping>?
?
?
?
??<filter-mapping>
?
????<filter-name>GzipFilter</filter-name>
?
????<url-pattern>*.js</url-pattern>
?
??</filter-mapping>
?
?
?
Demo样例3:Servlet类??
?
?
?
?
?
Demo样例4:Service类?
?
?
?
?
?
?
?
?
?
?
23、实用案例-缓存数据到内存
?
对于页面中很少更新的数据,例如商品分类,为避免每次都要从数据库查询分类数据,因此可把分类数据缓存在内存或文件中,以此来减轻数据库压力,提高系统响应速度。
?
?
?
Demo样例1:Filter类???
?
public?class?WebCacheFilter?implements?Filter?{
?
?
?
??private?Map<String,byte[]>?map?=?new?HashMap();
?
?
?
??public?void?doFilter(ServletRequest?req,?ServletResponse?resp,
?
??????FilterChain?chain)?throws?IOException,?ServletException?{
?
?
?
????HttpServletRequest?request?=?(HttpServletRequest)?req;
?
????HttpServletResponse?response?=?(HttpServletResponse)?resp;
?
?
?
????//1.得到用户想访问的资源(uri)
?
????String?uri?=?request.getRequestURI();
?
?
?
????//2.看map集合中是否保存了该资源的数据
?
????byte?b[]?=?map.get(uri);
?
?
?
????//3.如果保存了,则直接取数据打给浏览器
?
????if(b!=null){
?
??????response.getOutputStream().write(b);
?
??????return;
?
????}
?
?
?
????//4.如果没有保存数据,则放行让目标资源执行,这时还需写一个response的包装类,捕获目标资源的输出
?
????MyResponse?my?=?new?MyResponse(response);
?
????chain.doFilter(request,?my);
?
????byte?data[]?=?my.getBuffer();
?
?
?
????//5.以资源uri为关键字,打资源的数据保存map集合中,以备于下次访问
?
????map.put(uri,?data);
?
?
?
????//6.输出数据给浏览器
?
????response.getOutputStream().write(data);
?
??}
?
?
?
??class?MyResponse?extends?HttpServletResponseWrapper{
?
????private?ByteArrayOutputStream?bout?=?new?ByteArrayOutputStream();
?
????private?PrintWriter?pw;
?
?
?
????private?HttpServletResponse?response;
?
????public?MyResponse(HttpServletResponse?response)?{
?
??????super(response);
?
??????this.response?=?response;
?
????}
?
????@Override
?
????public?ServletOutputStream?getOutputStream()?throws?IOException?{
?
??????return?new?MyServletOutputStream(bout);????//myresponse.getOutputStream().write("hahah");
?
????}
?
?
?
????@Override
?
????public?PrintWriter?getWriter()?throws?IOException?{
?
??????pw?=?new?PrintWriter(new?OutputStreamWriter(bout,response.getCharacterEncoding()));
?
??????return?pw;??//MyResponse.getWriter().write("中国");
?
????}
?
?
?
????public?byte[]?getBuffer(){
?
??????if(pw!=null){
?
????????pw.close();
?
??????}
?
??????return?bout.toByteArray();
?
????}
?
??}
?
?
?
??class?MyServletOutputStream?extends?ServletOutputStream{
?
????private?ByteArrayOutputStream?bout;
?
????public?MyServletOutputStream(ByteArrayOutputStream?bout){
?
??????this.bout?=?bout;
?
????}
?
????@Override
?
????public?void?write(int?b)?throws?IOException?{
?
??????bout.write(b);
?
????}????
?
??}
?
?
?
??public?void?init(FilterConfig?filterConfig)?throws?ServletException?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
?
?
??public?void?destroy()?{
?
????//?TODO?Auto-generated?method?stub
?
??}
?
}
?
?
?
Demo样例2:web.xml文件??
?
?<filter>
?
????<filter-name>WebCacheFilter</filter-name>
?
????<filter-class>cn.itcast.web.filter.example.WebCacheFilter</filter-class>
?
??</filter>
?
?
?
??<filter-mapping>
?
????<filter-name>WebCacheFilter</filter-name>
?
????<url-pattern>/servlet/ServletDemo4</url-pattern>
?
??</filter-mapping>?
?
?
?
Demo样例3:Servlet类???
?
//显示分类的servlet
?
public?class?ServletDemo4?extends?HttpServlet?{
?
?
?
??public?void?doGet(HttpServletRequest?request,?HttpServletResponse?response)
?
??????throws?ServletException,?IOException?{
?
?
?
????//查询数据库,获取到分类信息
?
????String?data?=?"分类数据";????
?
?
?
????//输出分类信息
?
????response.getWriter().write(data);????
?
?
?
????System.out.println("hahahah");????
?
??}
?
?
?
??public?void?doPost(HttpServletRequest?request,?HttpServletResponse?response)
?
??????throws?ServletException,?IOException?{
?
????doGet(request,?response);
?
??}
?
}
?
?
?
?
?
?
?动态代理知识,未完待续……
?
?
?
敬请评论
(1)若您觉得本文 有用处? —— 请留言评论,以坚定其他 IT童鞋 阅读本文的信心。
(2)若您觉得本文 没用处? —— 请留言评论,笔者将会改进不足,以便为大家整理更加好用的笔记。
?
?
?
?
?
?
?