1、过滤器
过滤器,顾名思义,就是过滤器,可以将某批servlet组件中重复功能的代码提取到过滤器中统一编码和执行,可以减少重复编码,提高开发效率和软件的可维护性。
多个过滤器可以串联使用
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中的定义的先后顺序依次调用。
2、监听器
监听器,顾名思义,就是监听器,servlet可以创建监听器去监听其他对象的发生的事件,并在发生事件时采取响应的行动
servlet中监听对象主要是ServletContext, HttpSession和 ServletRequest 等域对象的创建和销毁事件,以及他们的属性发生修改的事件
ServletContextListener对象: ServletContext的监听器
ServletRequestListener对象: ServletRequest的监听器
ServletSessionListener对象: ServletSession的监听器
创建监听器方式
- 实现相应的的监听器
- web.xml中注册监听器
<listener>
<listener-class>com.class2.listener.MyListener</listener-class>
</listener>
3. 使用注解的方式配置Servlet
之前都是使用Web.xml的方式来配置我们的开发的Servlet程序, Tomcat启动的时候会加载web.xml文件的配置信息到内存当中,并以此信息来初始化Servlet实例
从Servlet3 版本开始,为了简化对Web组件的发布过程,可以不必再web.xml文件中配置web组件, 而是直接再相关的类中使用Annotation标注来配置发布信息。
- 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 “”; }
2. WebFilter注解
```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebFilter {
String description() default "";
String displayName() default "";
WebInitParam[] initParams() default {};
String filterName() default "";
String smallIcon() default "";
String largeIcon() default "";
String[] servletNames() default {};
String[] value() default {};
String[] urlPatterns() default {};
DispatcherType[] dispatcherTypes() default {DispatcherType.REQUEST};
boolean asyncSupported() default false;
}
- 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();
}
以上代码可以随机获取验证码返回客户端,例如:
问题:如何在服务端保存我们的验证码,用以校验
课堂代码: 使用单例模式的单例对象保存验证码
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异步处理机制,就是把响应请求的任务从一个线程传给另一个线程来处理。
下图为异步处理示意图:
5.2.2 异步处理流程
- 设置Servlet的配置
asyncSupported=true
- 主线程中
request.startAsync()
方法获取AsyncContext - 启动子线程,例如如下三种方式 :
- Runnable方式:
asyncContext.start(new MyTask(asyncContext))
,其中MyTask是一个Runnable的实现类 - Thread方式: new Thread(new MyTask(asyncContext)).start()
- 线程池方式: executor.execute(new MyTask(asyncContext))
- Runnable方式:
- 在子线程中调用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简介(以读数据为例,写数据同理)
- 阻塞式I/O:当线程在通过输入流执行读操作时,如果输入流的刻度数据暂时还未准备号,那么当前线程会进入阻塞状态,只有当读到了数据或者到达了数据末尾,线程才会从读方法中退出。
- 非阻塞式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);
}
}