学习JavaWeb(八)——Filter过滤器和Listener监听器

前言

继续学习JavaWeb,学到Filter过滤器和Listener监听器。

 

 

 

 

 


Filter

概念

过滤器是我们JavaWeb的三大组件之一,另外两个是Servlet、Listener

当访问服务器的资源时,过滤器可以将请求拦截下来,完成一些特殊的功能。

作用

一般用于完成通过的操作。如:登录验证、统一编码处理、敏感字符过滤

入门

编写过滤器的步骤:

  1. 定义一个类,实现接口Filter
  2. 复写方法
  3. 配置拦截路径
    1. web.xml
    2. 注解

创建一个java文件,使其继承于Filter(注意对应包是javax.servlet),其中doFilter方法就是对请求响应的拦截:

//注意,Filter所属包是:javax.servlet
@WebFilter("/*")   //访问所有资源之前都被拦截
public class FilterDemo1 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("filterDemo1被执行了");

        //放行
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {

    }
}

filterChain.doFilter就是对请求进行放行。

这样客户端就可以访问某个资源了,这个示例我们是通过注解配置的,配置的value值是”/*”,故访问任何的文件都会触发该过滤器。

 

web.xml的配置方式

上例我们使用了注解进行配置,比较简单,现在我们来说明一下web.xml文件的配置。

    <filter>
        <filter-name>demo1</filter-name>
        <filter-class>pers.luoluo.web.filter.FilterDemo1</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>demo1</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

这个效果就和注释@WebFilter(“/*”) 的效果是一样的,这种配置方法也和Servlet的配置方法几乎一样,只是url-pattern指的是拦截路径而不是访问路径。

过滤器执行流程

首先建议各位在IDEA的设置=》编辑器=》文件和代码模板–>Web–>Filter  Annotated   Class,然后修改Filter模板:

@javax.servlet.annotation.WebFilter("/*")
public class ${Class_Name} implements javax.servlet.Filter {
    public void doFilter(javax.servlet.ServletRequest req, javax.servlet.ServletResponse resp, javax.servlet.FilterChain chain) throws javax.servlet.ServletException, java.io.IOException {
        chain.doFilter(req, resp); //放行
    }
    public void init(javax.servlet.FilterConfig config) throws javax.servlet.ServletException {
    }
    public void destroy() {
    }
}

Filter的执行流程是在doFilter方法中,进入方法,然后放行,然后处理响应,即放行之后,服务端给客户端的响应再次被Filter拦截后是从放行那行之后开始执行的。

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        //对request对象请求消息进行增强
        System.out.println("filterDemo2执行了");

        chain.doFilter(req, resp);  //放行

        //对response对象的响应消息增强
        System.out.println("FilterDemo2回来了");
    }

假设我们的某个jsp页面有代码脚本会在控制台打印信息,打印“index.jsp…”,那么最后的打印顺序是:

filterDemo2执行了—>index.jsp…—>FilterDemo2回来了

生命周期

  • init:在服务器启动后,会创建Filter对象,然后调用init方法(一般用于加载资源)。
  • doFilter:每一次请求被拦截资源时,会执行(实现拦截效果)。
  • destroy:在服务器关闭后,Filter对象被销毁。如果服务器是正常关闭,则会执行destroy方法(一般用于释放资源)。

过滤器路径配置

拦截路径的配置

  • 具体资源路径:/index.jsp : 只有访问index.jsp资源时,过滤器才会被执行
  • 拦截目录:/user/* :访问/user下的所有资源时,过滤器都会被执行
  • 后缀名拦截:*.jsp:访问所有jsp资源时,过滤器都会被执行
  • 拦截所有资源:/*:访问所有资源时,过滤器都会被执行

拦截方式配置

  • 注解配置
    • 设置dispatcherTypes属性
      • REQUEST:默认值。浏览器直接请求资源
      • FORWARD:转发访问资源
      • INCLUDE:包含访问资源
      • ERROR:错误跳转资源
      • ASYNC:异步访问资源

测试,FilterDemo1:

@WebFilter(value = "/index.jsp",dispatcherTypes = DispatcherType.FORWARD)
public class FilterDemo3 implements Filter {

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        System.out.println("dofilter...");
        chain.doFilter(req, resp);  //放行
    }

    public void init(FilterConfig config) throws ServletException {
        System.out.println("init");
    }

    public void destroy() {
        System.out.println("destroy");
    }
}

另一个Servlet:

@WebServlet("/ServletDemo")
public class ServletDemo extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("ServletDemo");
        //转发到index.jsp
        request.getRequestDispatcher("/index.jsp").forward(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}

接下来,当我们直接访问index.jsp,并没有打印doFilter…,然后我们访问Servlet,Servlet帮助我们跳转到了index.jsp,然后控制台打印了doFilter…。

如果注解中想要配置多个拦截方式,可以装到一个数组中:

@WebFilter(value = "/index.jsp",dispatcherTypes = {DispatcherType.FORWARD,DispatcherType.REQUEST})
  • web.xml配置
    • 设置<dispatcher></dispatcher>标签即可

 

过滤器链(配置多个过滤器)

执行顺序

如果有两个过滤器:过滤器1和过滤器2,那么资源访问的话执行顺序就是:

  1. 过滤器1
  2. 过滤器2
  3. 资源
  4. 过滤器2
  5. 过滤器1

 可以理解为就像一个栈结构。

那么如何定义谁是1谁是2呢?

过滤器向后顺序问题

  • 注解配置:按照类名的字符串比较规则比较,值小的先进行
    • 如:AFilter和BFilter,AFilter就先执行了
  • web.xml配置:<filter-mapping>谁定义在上边,谁先执行

 

案例:登录

访问资源,验证其是否登录。

如果已经登录,则直接放行。

如果没有登录,则跳转到登录页面,提示“您尚未登录,请先登录”。

我们这里有一个简单的例子

  <body>
  <div style="margin-top: 8%">
    <form action="${pageContext.request.contextPath}/loginServlet" method="post">
      <div style="text-align: center">
        <label for="username">用户名</label>
        <input type="text" id="username" name="username" />
      </div>
      <div style="text-align: center">
        <label for="password">密码</label>
        <input type="text" id="password" name="password" />
      </div>
      <div style="text-align: center">
        <input type="submit" value="登录" />
      </div>
    </form>
    <div style="text-align: center">
      <span style="color: orangered">${requestScope.login_msg}</span>
    </div>
  </div>
  </body>

然后表单将数据发送给了Servlet——LoginServlet.java

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");

        String username=request.getParameter("username");
        String password=request.getParameter("password");

        if("tom".equals(username)&&"123".equals(password)){
            //登陆成功,重定向
            response.sendRedirect(request.getContextPath()+"/target.jsp");
        }else{
            //登录失败,请求转发
            request.setAttribute("login_msg","用户名或密码错误");
            request.getRequestDispatcher("/index.jsp").forward(request,response);
        }
    }

然后如果登录成功,就会跳转到target.jsp:

<body>
<div >
    <div style="text-align: center;margin-top: 3%">
        <h1>欢迎来到天堂!</h1>
        <p>这里是<span style="color: #400002">天堂</span>,只有平日积德的人才能进入这里!</p>
    </div>
</div>
</body>

目前没有Filter的代码来看,我们可以直接访问target.jsp,就可以来到登录后的页面,这显然是不允许的,接下来我们来实现需求:

我们先进行分析,我们的拦截器不能拦截和登录相关的资源,例如登录页面,如果访问登录页面需要你先登录,那么将无法登录。

首先我们在LoginServlet中给登录成功后设置一个session

            request.getSession().setAttribute("user","1");

创建web包下的filter包,创建一个LoginFilter,注解拦截“/*”

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        //0.强制转换
        HttpServletRequest request=(HttpServletRequest)req;
        
        //1.获取请求路径,判断是否与登录相关
        String uri = request.getRequestURI();
        //2.判断是否包含登录相关的路径
        if(uri.contains("/index.jsp")||uri.contains("loginServlet")){
            //包含,直接放行
            chain.doFilter(req, resp);  //放行
        }else{
            //不包含,需要验证
            //3.Session中获取信息
            Object user = request.getSession().getAttribute("user");
            if(user!=null){
                //当前会话已经登录,放行
                chain.doFilter(req, resp);  //放行
            }else{
                //没有登录
                request.setAttribute("login_msg","您尚未登录");
                request.getRequestDispatcher("index.jsp").forward(req,resp);
            }
        }
    }

这样,我们就实现了利用Filter和session实现对登录的拦截管控,注意,如果以后有css、图片等资源,请给步骤2后面的if添加条件:uri.contains(“/css/”)、uri.contains(“/image/”)…………

 

案例:过滤敏感词(动态代理)

基础项目

承接上例,登录的时候我们用户名输入“有意思”,然后登录成功后

我们这里有一个新项目,index.jsp:

<body>
  <div style="margin-top: 8%">
    <form action="${pageContext.request.contextPath}/resServlet" method="post">
      <div style="text-align: center">
        <label for="words">骂人的话:</label>
        <input type="text" id="words" name="words" style="width:190px;height: auto" />
      </div>
      <div style="text-align:center;margin-top: 3%">
        <input style="width: 80px;background-color: wheat;height: auto;"
               type="submit" value="发送" />
      </div>
    </form>
    <div style="text-align: center;margin-top: 5%">
      <span style="color: orangered">${requestScope.msg}</span>
    </div>
  </div>
  </body>

ResServlet.java:

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        String words = request.getParameter("words");
        request.setAttribute("msg",words);

        request.getRequestDispatcher("/index.jsp").forward(request,response);
    }

这样,我们在页面的文本框中填写内容后,然后点击提交,然后下面就会显示我们写的内容。

我们现在要让用户填写内容中的敏感词变成****,改怎么做呢?

思路

我们用Filter去拦截,拦截后对request对象处理,增强这个request对象成为一个新的request对象,然后在放行的时候将新的request对象写入。

代理模式

这里我们应用二十三式设计模式之代理模式来解决问题,实现对request的增强。

代理模式:

  • 概念
    • 真实对象:被代理的对象
    • 代理对象:代理真实对象的对象
    • 代理模式:代理对象代理真实对象,达到增强真实对象功能的目的
  • 实现方式
    • 静态代理:有一个类文件描述代理模式
    • 动态代理:在内存中形成代理类
      • 实现步骤:
        • 代理对象和真实对象实现相同的接口
        • 获取代理对象:Proxy.newProxyInstance()
        • 使用代理对象来调用方法

动态代理示例:

public interface Country {
    void show();
}
public class China implements Country{
    @Override
    public void show() {
        System.out.println("这里是中国!");
    }
}

代理测试类:

public class ProxyTest {
    public static void main(String[] args) {
        //1.创建真实对象
        China cn=new China();
        //2.动态代理增强对象
        /*
            三个参数:
                1.类加载器:得到真实对象 getClass().getClassLoader()
                2.接口数组:确保真实对象和代理对象同样的接口 getClass().getInterfaces()
                3.处理器:InvocationHandler(),匿名内部类来实现
         */
        Country proxy_cn = (Country)Proxy.newProxyInstance(cn.getClass().getClassLoader(),
                cn.getClass().getInterfaces(),
                new InvocationHandler() {
                    /*
                        代理逻辑编写的方法:代理对象调用的所有方法都会触发该方法执行
                         参数
                           1.proxy:代理对象
                           2.method:代理对象调用的方法被封装为的对象
                           3.args:代理对象调用方法时,传递的实际参数
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args)
                            throws Throwable {
                        System.out.println("invoke方法执行了");
                        //调用真实对象执行该方法
                        method.invoke(cn);
                        return null;
                    }
                });
        //2.调用方法
        proxy_cn.show();
    }
}

如同我注释上面写的一样,Proxy.newProxyInstance方法需要三个参数:

  1. 类加载器:得到真实对象 getClass().getClassLoader()
  2. 接口数组:确保真实对象和代理对象同样的接口 getClass().getInterfaces()
  3. 处理器:InvocationHandler(),匿名内部类来实现

学英语:invocation:求助、调用、启用

得到的代理对象调用方法其实是调用了InvocationHandler中的方法invoke,我们在invoke中实现对方法的执行以及其他功能。

invoke方法参数:

  1. proxy:代理对象
  2. method:代理对象调用的方法被封装为的对象
  3. args:代理对象调用方法时,传递的实际参数

这样,我们就可以在invoke中进行各种的增强处理。

过滤敏感词

了解了动态代理,我们来实现过滤敏感词汇。

在src目录下创建一个txt文件,每行写一个敏感词,然后创建我们的Filter文件,内容如下:

@WebFilter("/*")
public class SensitiveWordsFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        //1.创建代理对象,增强getParameter方法
        ServletRequest proxy_req = (ServletRequest) Proxy.newProxyInstance(req.getClass().getClassLoader(),
                req.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args)
                            throws Throwable {
                        //增强getParametrer方法
                        if(method.getName().equals("getParameter")){
                            //增强返回值
                            //获取返回值
                            String value=(String)method.invoke(req,args);
                            if(value!=null){
                                for(String str:list){
                                    if(value.contains(str)){
                                        value=value.replaceAll(str,"**");
                                    }
                                }
                            }
                            return value;
                        }
                        return method.invoke(req,args);
                    }
                });
        //2.放行
        chain.doFilter(proxy_req, resp);  //放行
    }
    private List<String> list=new ArrayList<String>(); //敏感词汇集合
    public void init(FilterConfig config) throws ServletException {
        BufferedReader br=null;
        try{
            //1.加载文件(src目录下)
            String realPath = config.getServletContext().getRealPath("/WEB-INF/classes/sensitive.txt");
            //2.读取文件
            br = new BufferedReader(new InputStreamReader(
                             new FileInputStream(realPath),"UTF-8"));
            //3.将文件的每一行数据添加到list中
            String line=null;
            while((line=br.readLine())!=null){
                list.add(line);
            }
            System.out.println(list);
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public void destroy() {

    }
}

效果

同理对于getParameterMap、getParameterValue的具体实现这里就不列出来了。

 


Listener监听器

其实我们之前的JavaWeb中聊过监听器了,这里我们再聊一下也无妨。

概念

Listener是JavaWeb的三大组件之一。

事件监听机制

  • 事件:一件事情。
  • 事件源:事件发生的地方。
  • 监听器:一个对象。
  • 注册监听:将事件、事件源、监听器绑定在一起。当事件源上发生某个事件后,执行监听器代码。

 

在我们的JavaWeb中,有很多的监听器,这里我们先主要学习一个——ServletContextListener

ServletContextListener

ServletContextListener

ServletContextListener是一个接口,监听ServletContext对象的创建与销毁。

  • contextDestroyed(ServletContextEvent sce):Servlet对象被销毁之前会调用该方法
  • contextInitialized(ServletContextEvent sce):ServletContext对象创建后会调用该方法

使用步骤

  1. 定义一个类,实现ServletContextListener
  2. 复写方法
  3. 配置

示例,我们在web包下创建一个Listener包,然后创建一个java文件——ContextLoaderListener

public class ContextLoaderListener implements ServletContextListener {
    /**
     * 监听ServletContext对象创建的。ServletContext对象服务器启动后自动创建
     **/
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("ContextServlet对象被创建了!");
    }
    /**
     * 服务器关闭后,ServletContext对象被销毁。
     * 当服务器正常关闭后,该方法被调用。
     **/
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("ContextServlet对象被销毁了!");
    }
}

然后我们在WEB-INF下的web.xml中进行配置:

    <!--配置监听器:注册监听-->
    <listener>
        <listener-class>pers.luoluo.web.listener.ContextLoaderListener</listener-class>
    </listener>

这样,我们启动项目和关闭项目(正常关闭),就会分别打印信息。

用途示例

那请问这监听器有什么用呢?其实这种类似“生命周期方法”,主要的用处就是进行一些资源的初始化。

我们来举个例子,来加载某个配置文件进入内存,例如在src目录下存在文件applicationContext.xml,然后在web.xml中有设置参数:

    <!--指定初始化参数信息-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/classes/applicationContext.xml</param-value>
    </context-param>

这个参数键值对可以通过ServletContext访问。

故我们在监听器中,可以通过servletContextEvent得到ServletContext

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //加载资源文件
        //1.获取ServletContext对象
        ServletContext servletContext=servletContextEvent.getServletContext();
        //2.加载资源文件
        String contextConfigLocation = servletContext.getInitParameter("contextConfigLocation");
        //3.获取真实路径
        String realPath = servletContext.getRealPath(contextConfigLocation);
        //4.加载进内存
        try {
            FileInputStream fls=new FileInputStream(realPath);
            System.out.println(fls);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("ContextServlet对象被创建了!");
    }

当我们启动项目的时候,打印:

java.io.FileInputStream@28ae693f

即说明文件流被成功创建,可以读取文件信息。

 

 

 


 

 

 

 

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

 

发表评论