1、过滤器

过滤器,顾名思义,就是过滤器,可以将某批servlet组件中重复功能的代码提取到过滤器中统一编码和执行,可以减少重复编码,提高开发效率和软件的可维护性。
image.png多个过滤器可以串联使用

1.1 Filter接口

方法 描述
init(FilterConfig filterConfig) 初始化方法,在Web应用启动的时候,servlet容器先创建包含了过滤器配置信息的FilterConfig对象,然后在创建Filter对象,接着嗲用Filter对象的init方法。参数filterConfig封装好了过滤器的初始化参数
doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) 完成实际的过滤业务操作,请求URL时,先调用filter的doFilter方法,然后再通过FilterChain参数的doFilter方法进行请求转发,做后续处理
destory() 释放资源

2.2 串联过滤器

过滤器可以将其串联起来使用,按照web.xml中的定义的先后顺序依次调用。
image.png

2、监听器

监听器,顾名思义,就是监听器,servlet可以创建监听器去监听其他对象的发生的事件,并在发生事件时采取响应的行动

servlet中监听对象主要是ServletContext, HttpSession和 ServletRequest 等域对象的创建和销毁事件,以及他们的属性发生修改的事件

ServletContextListener对象: ServletContext的监听器
ServletRequestListener对象: ServletRequest的监听器
ServletSessionListener对象: ServletSession的监听器

创建监听器方式

  1. 实现相应的的监听器
  2. web.xml中注册监听器
  1. <listener>
  2. <listener-class>com.class2.listener.MyListener</listener-class>
  3. </listener>

3. 使用注解的方式配置Servlet

之前都是使用Web.xml的方式来配置我们的开发的Servlet程序, Tomcat启动的时候会加载web.xml文件的配置信息到内存当中,并以此信息来初始化Servlet实例

从Servlet3 版本开始,为了简化对Web组件的发布过程,可以不必再web.xml文件中配置web组件, 而是直接再相关的类中使用Annotation标注来配置发布信息。

  1. WebServlet注解 ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface WebServlet { String name() default “”; String[] value() default {}; String[] urlPatterns() default {}; int loadOnStartup() default -1; WebInitParam[] initParams() default {}; boolean asyncSupported() default false; String smallIcon() default “”; String largeIcon() default “”; String description() default “”; String displayName() default “”; }
  1. 2. WebFilter注解
  2. ```java
  3. @Target({ElementType.TYPE})
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Documented
  6. public @interface WebFilter {
  7. String description() default "";
  8. String displayName() default "";
  9. WebInitParam[] initParams() default {};
  10. String filterName() default "";
  11. String smallIcon() default "";
  12. String largeIcon() default "";
  13. String[] servletNames() default {};
  14. String[] value() default {};
  15. String[] urlPatterns() default {};
  16. DispatcherType[] dispatcherTypes() default {DispatcherType.REQUEST};
  17. boolean asyncSupported() default false;
  18. }
  1. WebListener
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface WebListener {
     String value() default "";
    }
    

4. 文件的上传下载

4.1 文件上传

(略,利用Apache开源类库实现文件上传,后续学习框架springmvc中再统一讲)

4.2 下载下载

下载文件是指把服务器端的文件发送到客户端。Servlet能够向客户端发送任意格式的文件数据。

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    OutputStream out;
    InputStream in;

    String filename = req.getParameter("filename");
    if (filename == null) {
        out = resp.getOutputStream();
        out.write("please input filename.".getBytes());
        out.close();
        return;
    }

    in = getServletContext().getResourceAsStream("/file/" + filename);
    int length = in.available();

    resp.setContentType("application/force-download");          //指定响应类型为下载
    resp.setHeader("Content-Length", String.valueOf(length));   //指定文件的大小
    resp.setHeader("Content-Disposition", "attachment;filename=\""+filename+"\"");  //指定下载的文件名

    out = resp.getOutputStream();
    int bytesRead = 0;
    byte[] buffer = new byte[1024];
    while ((bytesRead = in.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
    }

    in.close();
    out.close();
}

4.3 模拟生成随机验证码

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    /**
         * 1.绘图
         */
    Random r = new Random();
    //创建一个位于缓冲区中的图像,
    BufferedImage image = new BufferedImage(90, 20, BufferedImage.TYPE_INT_RGB);
    //获得Graphics画笔
    Graphics g = image.getGraphics();

    g.setColor(new Color(255, 255, 255));
    //画一个矩形
    g.fillRect(0, 0, 90, 30);
    //随机设置画笔的颜色, 再设置画笔的字体,风格和大小
    g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
    g.setFont(new Font("微软雅黑", Font.BOLD | Font.ITALIC, 22));

    //生成一个随机的六位数
    String code = r.nextInt(1000000) + "";
    System.out.println("code = " + code);   //打印再控制台
    g.drawString(code, 0, 20);

    //随机画八条干扰线
    for (int i = 0; i < 8; i++) {
        g.setColor(new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)));
        g.drawLine(r.nextInt(90), r.nextInt(20), r.nextInt(90), r.nextInt(20));
    }

    /**
         * 2.将图片写回客户端
         */
    response.setContentType("image/jpeg");
    ServletOutputStream out = response.getOutputStream();
    ImageIO.write(image, "jpeg", out);
    out.close();
}

以上代码可以随机获取验证码返回客户端,例如:
image.png

问题:如何在服务端保存我们的验证码,用以校验
课堂代码: 使用单例模式的单例对象保存验证码

5. Servlet中提供的并发问题的解决方案

5.1 Servlet的线程不安全问题描述

在Internet中,一个Web应用可能被来自四面八方的客户并发访问(即同时访问), 而且有可能这些客户并发访问的是Web应用中的同一个Servlet。Servlet容器为了保证能同时响应多个客户的要求访问同一个Servlet的Http请求,通常会为每个请求分配一个工作线程,这些工作线程并发执行同一个Servlet对象的service()方法,去修改多个线程均可见的共享变量时,就可能会导致并发问题。

案例代码同Runnable接口实现线程的多窗口售票问题。解决方案也是相同,就是利用java的同步机制来解决。

5.2 对客户请求的异步处理

5.2.1 异步处理概述

在ServletAPI 3.0版本之前,Servlet容器针对每个HTTP请求都会分配一个工作线程。即对于每一次HTTP请求,Servlet容器都会从主线程池中取出一个空闲的工作线程,由该线程从头到尾负责处理请求,如果在相应某个HTTP请求的过程中涉及到进行I/O操作,访问数据库,或者其他耗时操作,那么该工作线程会被长时间占用,只有当工作线程完成了对当前HTTP请求的响应,才会释放回线程池以供后续请求使用。

在并发访问量很大的情况下,如果线程池中的许多工作线程都被长时间占用,这将严重影响服务器的并发访问性能,为了解决这种问题,从ServletAPI 3 开始,引入了异步处理机制,随后Servlet API 3.0中引入了非阻塞I/O类进一步增强异步处理能力。

Servlet异步处理的机制为: Servlet从HttpServletRequest对象中获取一个AsyncContext对象,该对象表示异步处理的上下文。AsyncContext把响应当前请求的任务传给一个新的线程,由这个新的线程来完成对请求的处理并向客户端返回响应结果。最初由Servlet容器为Http请求分配的工作线程便可以及时地释放回主线程翅,从而及时处理更多的请求。

总而言之,Servlet异步处理机制,就是把响应请求的任务从一个线程传给另一个线程来处理。
下图为异步处理示意图:
image.png

5.2.2 异步处理流程

  • 设置Servlet的配置asyncSupported=true
  • 主线程中request.startAsync()方法获取AsyncContext
  • 启动子线程,例如如下三种方式 :
    1. Runnable方式:asyncContext.start(new MyTask(asyncContext)),其中MyTask是一个Runnable的实现类
    2. Thread方式: new Thread(new MyTask(asyncContext)).start()
    3. 线程池方式: executor.execute(new MyTask(asyncContext))
  • 在子线程中调用complete()方法告知servlet容器任务完成,返回响应结果

AsyncContext接口部分源码

public interface AsyncContext {
    ServletRequest getRequest();

    ServletResponse getResponse();

    void dispatch(String var1);

    void complete();

    void start(Runnable var1);

    void addListener(AsyncListener var1);

    void addListener(AsyncListener var1, ServletRequest var2, ServletResponse var3);

    void setTimeout(long var1);
}

5.2.3 异步监听器AsyncListener

除了ServletContext, HttpSession和ServletRequest等有监听器之外,我们的异步机制也有其独有的监听器AsyncListener,该接口有四个方法,源码如下

public interface AsyncListener extends EventListener {
    void onComplete(AsyncEvent var1) throws IOException; 异步线程执行完毕时调用

    void onTimeout(AsyncEvent var1) throws IOException;    //异步线程超时时调用

    void onError(AsyncEvent var1) throws IOException;    //异步线程出错时调用

    void onStartAsync(AsyncEvent var1) throws IOException; //异步线程开始时调用
}

使用asyncContext的addListener(AsyncListener asycnListener)来在代码中注册该监听器

5.2.4 非阻塞式I/O的引入

目的和上述的异步是一致的,只是这里使用了非阻塞式I/O模型,当异步线程利用IO流读写大量数据时,会使得异步线程也处于阻塞状态(如超大附件上传,下载),异步线程也阻塞住了同样会削弱服务器的并发访问的能力,所以Servlet API在3.1开始,引入了非阻塞I/O机制,它建立在异步处理的基础之上。

阻塞式I/O和非阻塞式I/O简介(以读数据为例,写数据同理)

  1. 阻塞式I/O:当线程在通过输入流执行读操作时,如果输入流的刻度数据暂时还未准备号,那么当前线程会进入阻塞状态,只有当读到了数据或者到达了数据末尾,线程才会从读方法中退出。
  2. 非阻塞式I/O: 当线程在通过输入流执行 读操作时,如果发现输入流的可读数据还未准备号,那么当前线程不会进入阻塞状态,而是退出读方法,使其可以去马上执行其他任务,而不是阻塞在那里等待I/O完成。当数据准备完成后,再改变I/O状态,通知系统分配线程来处理。

servlet中引入的非阻塞式I/O模型

主要时引入了两个监听器:
ReadListener接口: 监听ServletInputStream输入流行为。
WriteListener接口: 监听ServletOutputStream输出流行为。

这里只介绍ReadListener,它的源码如下:

public interface ReadListener extends EventListener {
    void onDataAvailable() throws IOException;    //输入流中有可读数据时出发此方法

    void onAllDataRead() throws IOException;    //输入流中所有数据读完时出发此方法

    void onError(Throwable var1);    //输入流出现错误时出发此方法
}

基本使用:
同样需要获取异步对象AsyncContext
在主Servlet类中通过request获取ServletInputStream
并且设置监听器ReadListener接口

AsyncContext asyncContext = request.startAsync(); 

ServletInputStream inputStream = request.getInputStream();
inputStream.setReadListener(new MyReadListener(inputStream, asyncContext));

其中MyReadListener实现了ReadListener接口,我的案例代码实现如下

class MyReadListener implements ReadListener {
    private ServletInputStream servletInputStream;
    private AsyncContext asyncContext;
    private StringBuilder sb = new StringBuilder();
    public MyReadListener(ServletInputStream inputStream, AsyncContext context) {
        this.servletInputStream = inputStream;
        this.asyncContext = context;
    }

    //输入流中有可读数据会调用此方法
    @Override
    public void onDataAonvailable() throws IOException {
        try {
            System.out.println("流中有可用数据" + LocalTime.now());
            //模拟数据读取5秒
            TimeUnit.SECONDS.sleep(5);
            int len;
            byte[] buffer = new byte[1024];
            while (servletInputStream.isReady()
                    && (len = servletInputStream.read(buffer)) > 0) {
                String data = new String(buffer, 0, len);
                sb.append(data);
            }

            System.out.println("流中数据读取结束" + LocalTime.now());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onAllDataRead() throws IOException {
        System.out.println("数据读取完成" + LocalTime.now());

        asyncContext.getRequest().setAttribute("msg", sb.toString());
        asyncContext.dispatch("/output");
    }

通过前端上传文件后,会出发onDataAonvailable()方法,所有数据读取完毕后会自动调用onAllDataRead() 方法,并在这个方法中调用asyncContext.dispatch(path); 来将请求派发给另一个servlet来处理。

6. 嵌入式服务器简介

目前比较流行的框架Springboot集成Web项目就是使用嵌入式的Tomcat服务器,即无需外部tomcat服务器来启动加载我们的web项目,而是将tomcat服务器嵌入我们的java项目当中,将其当作进程内的Servlet容器来运行,可以使得java应用程序更为灵活的控制Servlet容器。

下面我写了一个简单的通过java的main方法启动嵌入式tomcat服务器,并提供服务

public class Application {
    private static Tomcat tomcat;
    private static final int DEFAULT_PORT = 8080;   //默认端口

    //服务器启动
    public static void run(int port){
        tomcat = new Tomcat();

        //设置服务器以及虚拟主机的根路径
        tomcat.setBaseDir(".");
        tomcat.getHost().setAppBase(baseDir);

        //设置tomcat接受Http请求的监听端口号
        tomcat.setPort(port);

        //获取连接器,如果没有连接器,会获得一个默认的连接器
        Connector connector = tomcat.getConnector();

        //加入服务器默认的web应用,即在页面输入localhost:8080后默认访问的程序
        //第二个参数可以等同于外部tomcat下webapps下的ROOT项目的路径
        Context context1 = tomcat.addWebapp("", "D:\\developers\\javadeveloper\\workspace\\idea\\servlet\\out\\artifacts\\web_war_exploded");

        //addWebapp可以重复添加项目到tomcat容器中

        //启动tomcat
        try {
            tomcat.start();
        } catch (LifecycleException e) {
            e.printStackTrace();
        }

        //阻塞此线程,让其一直存在于后台
        StandardServer server = (StandardServer) tomcat.getServer();
        server.await();
    }

    public static void main(String[] args) {
        Application.run(Application.DEFAULT_PORT);
    }
}