以下转载和参考自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里适合放置一些初始化、资源清理销毁。