Servlet:Filter和Listener、部署【转】

以下转载和参考自Servlet进阶 - 廖雪峰的官方网站

1、Filter

  使用Filter可以在Servlet处理用户的请求之前做指定的事情,比如用户浏览/user/下的页面需要先登录,那么可以如下定义一个Filter,@WebFilter注解标注该Filter需要过滤的路径,所以如果想要客户浏览所有页面都得登录的话那么指定@WebFilter为/*。

@WebFilter("/user/*")
public class AuthFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        if (req.getSession().getAttribute("user") == null) {
            // 未登录,自动跳转到登录页:
            System.out.println("AuthFilter: not signin!");
            resp.sendRedirect("/signin");
        } else {
            // 已登录,继续处理:
            chain.doFilter(request, response);
        }
    }
}

    在doFilter()方法内部,要继续处理请求,必须调用chain.doFilter(),如果不调用该方法的话,那么就不会执行后续的Filter以及Servlet服务。

   可以定义多个Filter,这些Filter会依次执行,如两个Filter为@WebFilter("/user/*")和@WebFilter("/*"),那么如果一个请求路径类似/user/profile,那么它会被这两个Filter依次处理。Servlet规范并没有对@WebFilter注解标注的Filter规定顺序。如果一定要给每个Filter指定顺序,就必须在web.xml文件中对这些Filter再配置一遍。

  Filter适用于登录检查、日志、全局设置等,添加了@WebFilter("/*")的Filter之后,整个请求的处理架构如下:

   如果我们使用上一节介绍的Spring MVC模式,即一个统一的DispatcherServlet入口,加上一个或多个Controller,这种模式下Filter仍然是正常工作的。例如,一个处理/user/*的Filter实际上作用于那些处理/user/开头的Controller方法之前。

2、使用Filter替换请求

  如果在Filter中调用了HttpServletRequest的getInputStream() / getReader()读取了数据,那么后续在Servlet中再次读取请求中的数据的话,将无法读到任何数据。我们可以实现一个HttpServletRequestWrapper,将在Filter中读取了的数据传给它,然后重写getInputStream() / getReader()为从传入的数据获得,在Filter中调用chain.doFilter()的时候传入这个HttpServletRequestWrapper来替换原来的HttpServletRequest( HttpServletRequestWrapper实现了),如下所示:

class ReReadableHttpServletRequest extends HttpServletRequestWrapper {
    private byte[] body;
    private boolean open = false;

    public ReReadableHttpServletRequest(HttpServletRequest request, byte[] body) {
        super(request);
        this.body = body;
    }

    // 返回InputStream:
    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (open) { //InputStream只能获取一次,即body数据只能读取一次
            throw new IllegalStateException("Cannot re-open input stream!");
        }
        open = true;
        return new ServletInputStream() {
            private int offset = 0;

            public boolean isFinished() {
                return offset >= body.length;
            }

            public boolean isReady() {
                return true;
            }

            public void setReadListener(ReadListener listener) {
            }

            public int read() throws IOException {
                if (offset >= body.length) {
                    return -1;
                }
                int n = body[offset] & 0xff; //与0xff与感觉没有意义
                offset++;
                return n;
            }
        };
    }

    // 返回Reader:
    @Override
    public BufferedReader getReader() throws IOException {
        if (open) {
            throw new IllegalStateException("Cannot re-open reader!");
        }
        open = true;
        return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), "UTF-8"));
    }
}
public class UploadFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ......
        chain.doFilter(new ReReadableHttpServletRequest(req, output.toByteArray()/*读取的数据*/), response);
    }
}

  上面的做法使用的是代理模式:我们编写ReReadableHttpServletRequest时,是从HttpServletRequestWrapper继承,而不是直接实现HttpServletRequest接口。这是因为,Servlet的每个新版本都会对HttpServletRequest接口增加一些新方法,HttpServletRequestWrapper是由Servlet的jar包提供的,目的就是为了让我们方便地实现对HttpServletRequest接口的代理。 

3、使用Filter修改响应

  我们能通过Filter修改HttpServletRequest,自然也能修改HttpServletResponse。如下所示的HelloServlet,每次请求返回的数据都从getResponseData()计算,如果每次计算数据其实都是相同的话,每次返回的响应内容是固定的,那么我们就可以将getResponseData()获得的数据缓存起来。缓存逻辑最好不要在Servlet内部实现,因为我们希望能复用缓存逻辑,比如路径为"/slow/hello2"的Servlet也能使用该缓存策略,所以编写一个Filter来实现缓存逻辑是最合适的。

@WebServlet(urlPatterns = "/slow/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");

        String strData = getResponseData();

        PrintWriter pw = resp.getWriter();
        pw.write(strData);
        pw.flush();
    }
}

    如下的Filter中,我们判断是否存在缓存,存在的话即直接将其发送给浏览器,如果不存在的话,我们创建一个代理response后调用chain.doFilter()来使后续的Servlet使用该代理reponse,这样Servlet将缓存数据写入到了代理reponse中,然后我们可以获得这些数据来缓存起来 。 

@WebFilter("/slow/*")
public class CacheFilter implements Filter {
    private Map<String, byte[]> cache = new ConcurrentHashMap<>(); //保存缓存数据

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

        String url = req.getRequestURI();// 获取Path
        byte[] data = this.cache.get(url); // 获取缓存内容
        if (data == null) { //浏览器第一次请求数据的时候,缓存不存在,构造代理Response给Servlet后从代理Response获得向Response写入的数据作为缓存
            CachedHttpServletResponse wrapper = new CachedHttpServletResponse(resp); // 构造一个代理的Response
            chain.doFilter(request, wrapper); // 让下游Servlet组件写入数据到代理Response,doFilter()会直到HelloServlet的doGet()/doPost()调用完后才返回
            data = wrapper.getContent(); //获取缓存数据
            cache.put(url, data); //保存缓存数据
        }

        //浏览器非第一次请求数据的话,不用再使用Servlet处理请求,直接将缓存的数据写入到原始的Response作为应答
        ServletOutputStream output = resp.getOutputStream();
        output.write(data);
        output.flush();
    }
}
class CachedHttpServletResponse extends HttpServletResponseWrapper {
    private boolean open = false;
    private ByteArrayOutputStream output = new ByteArrayOutputStream();

    public CachedHttpServletResponse(HttpServletResponse response) {
        super(response);
    }

    // 获取写入到output的Writer:
    @Override
    public PrintWriter getWriter() throws IOException {
        if (open) { //写入数据后就不能再写入
            throw new IllegalStateException("Cannot re-open writer!");
        }
        open = true;
        return new PrintWriter(output, false, StandardCharsets.UTF_8);
    }

    // 获取写入到output的OutputStream:
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (open) {
            throw new IllegalStateException("Cannot re-open output stream!");
        }
        open = true;
        return new ServletOutputStream() {
            public boolean isReady() {
                return true;
            }

            public void setWriteListener(WriteListener listener) {
            }

            // 实际写入ByteArrayOutputStream:
            public void write(int b) throws IOException {
                output.write(b);
            }
        };
    }

    // 返回写入数据的byte[]:
    public byte[] getContent() {
        return output.toByteArray();
    }
}

4、Listener

   JavaEE的Servlet规范除了Servlet和Filter外,还有一个Listener。

   下面标注为@WebListener并且实现了ServletContextListener接口的类中,contextInitialized()方法在初始化WebApp的时候会被调用,可以把初始化数据库连接池等工作放到这里面,因为Web服务器保证在contextInitialized()执行后,才会接受用户的HTTP请求, contextDestroyed()方法在清理WebApp时会调用,可以把把清理资源的工作放到这里面。

     一个Web服务器可以运行一个或多个WebApp(war包),对于每个WebApp,Web服务器都会为其创建一个全局唯一的ServletContext实例,ServletContextListener中的contextInitialized()、contextDestroyed() 实际上对应的就是ServletContext实例的创建和销毁。ServletContext实例最大的作用就是设置和共享全局信息,除了ServletContextEvent,ServletRequest、HttpSession等很多对象也提供getServletContext()方法获取到同一个ServletContext实例。

   ServletContextListener的功能跟Filter有点类似,但ServletContextListener 是在Web App启动之前开始的,Filter则是在指定Servlet的doGet、doPost之前进行的。

@WebListener
public class AppListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        System.out.println("WebApp initialized: ServletContext = " + context);

        context.setAttribute("name", "object");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("WebApp destroyed.");
    }
}

除了ServletContextListener外,还有几种Listener:

  • HttpSessionListener:监听HttpSession的创建和销毁事件;
  • ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
  • ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用ServletRequest.setAttribute()方法);
  • ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用ServletContext.setAttribute()方法);

5、部署

  如下为一个Web应用程序的合理组织文件结构:

webapp
├── pom.xml
└── src
    └── main
        ├── java
        │   └── xsl
        │           └── learnjava
        │               ├── Main.java
        │               ├── filter
        │               │   └── EncodingFilter.java
        │               └── servlet
        │                   ├── FileServlet.java
        │                   └── HelloServlet.java
        ├── resources
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            ├── favicon.ico
            └── static
                    ├── css
                    │   └── bootstrap.css
                    └── js
                         └── jquery.js

 我们把所有的静态资源文件放入/static/目录,在开发阶段,有些Web服务器会自动为我们加一个专门负责处理静态文件的Servlet,但如果Servlet映射路径为/,会屏蔽掉处理静态文件的Servlet映射。因此,我们需要自己编写一个处理静态文件的FileServlet:

@WebServlet(urlPatterns = { "/static/*" })
 class FileServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext ctx = req.getServletContext();
        String strContextPath = ctx.getContextPath(); // 获取上下文路径,如"/warName",ROOT包的话为空
        String urlPath = req.getRequestURI(); //获取RequestURI,如"/HELLO/hello",ROOT包的话为"/hello"
        String path = urlPath.substring(strContextPath.length()); // RequestURI包含ContextPath,需要去掉

        String filepath = ctx.getRealPath(path); // 获取真实文件路径
        if (filepath == null) { // 无法获取到路径
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Path path = Paths.get(filepath);
        if (!path.toFile().isFile()) { // 文件不存在
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String mime = Files.probeContentType(path); // 根据文件名猜测Content-Type
        if (mime == null) {
            mime = "application/octet-stream";
        }
        resp.setContentType(mime);

        // 读取文件并写入Response
        OutputStream output = resp.getOutputStream();
        try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
            input.transferTo(output);
        }

        output.flush();
    }
}

  类似Tomcat这样的Web服务器,运行的Web应用程序通常都是业务系统,这类服务器也被称为应用服务器,应用服务器并不擅长处理静态文件,也不适合直接暴露给用户。通常,我们总是使用类似Nginx这样的服务器充当静态服务器以及提供反向代理服务,如下所示为部署架构以及Nginx配置文件的设置。使用Nginx配合Tomcat服务器,可以充分发挥Nginx作为网关的优势,既可以高效处理静态文件,也可以把https、防火墙、限速、反爬虫等功能放到Nginx中,使得我们自己的WebApp能专注于业务逻辑。

​
server {
    listen 80;

    server_name www.local.liaoxuefeng.com;

    # 静态文件根目录:
    root /path/to/src/main/webapp;

    access_log /var/log/nginx/webapp_access_log;
    error_log  /var/log/nginx/webapp_error_log;

    # 处理静态文件请求:
    location /static {
    }

    # 处理静态文件请求:
    location /favicon.ico {
    }

    # 不允许请求/WEB-INF:
    location /WEB-INF {
        return 404;
    }

    # 其他请求转发给Tomcat:
    location / {
        proxy_pass       http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

​

6、总结

   使用Filter可以提前对指定路径的请求进行拦截,然后决定是否放行,具体为实现Filter接口,并在该实现类上使用注解@WebFilter来指定要拦截的路径,如@WebFilter("/test")。

   在Filter中可以使用HttpServletRequestWrapper、HttpServletResponseWrapper来替换原来的HttpServletRequest和HttpServletResponse,比如我们想要对用户发来的数据进行一些额外处理然后再交给Servlet的话,就可以使用HttpServletRequestWrapper,再比如我们想要获取对Servlet写入的应答数据的话,可以使用HttpServletResponseWrapper。

   可以使用Listener来在Web App初始化和销毁的时候进行指定的操作,Filter是在指定Servlet的doGet、doPost之前进行的,Listener 是在Web App启动之前和销毁的时候开始的,所以Listener里适合放置一些初始化、资源清理销毁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值