Servlet转发JSP触发StackOverflowError,求跳过映射的方法
你分析的问题根源完全正确:当Servlet使用<url-pattern>/*</url-pattern>时,它的匹配优先级高于默认JSP Servlet的*.jsp扩展名匹配,所以所有请求(包括访问JSP的请求)都会先进入你的FullTracking Servlet。当你转发到/FullTracking.jsp时,这个请求又会被同一个Servlet拦截,形成无限循环,最终触发StackOverflowError。
下面给你几个可行的解决方案,按推荐程度排序:
方案1:使用Filter代替Servlet实现统计(最优解)
Servlet的映射会拦截所有请求调度类型(包括外部请求、转发、包含等),而Filter可以精准控制拦截的请求类型,完美避免循环问题。
步骤1:创建统计Filter
package com.tests; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Hashtable; public class FullTrackingFilter implements Filter { // 用Hashtable保证线程安全,Servlet/Filter是单例,多线程共享此对象 private Hashtable<String, Integer> links = new Hashtable<>(); @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化操作(可选) } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String uri = req.getRequestURI(); String resource = uri.substring(req.getContextPath().length()); // 跳过统计页面本身,避免不必要的统计 if (!"/FullTracking.jsp".equals(resource)) { // 同步操作确保统计计数的原子性 synchronized (links) { links.put(resource, links.getOrDefault(resource, 0) + 1); } } // 将统计数据放入请求,供JSP读取 req.setAttribute("links", links); // 放行请求,继续执行后续的Servlet/JSP chain.doFilter(request, response); } @Override public void destroy() { // 销毁操作(可选) } }
步骤2:配置Filter并指定拦截类型
在web.xml中替换原来的Servlet配置为Filter配置:
<filter> <filter-name>FullTrackingFilter</filter-name> <filter-class>com.tests.FullTrackingFilter</filter-class> </filter> <filter-mapping> <filter-name>FullTrackingFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 只拦截外部发起的请求,内部转发/包含不触发Filter --> <dispatcher>REQUEST</dispatcher> </filter-mapping>
步骤3:保留原JSP无需修改
你的FullTracking.jsp可以继续使用原来的代码,访问/FullTracking.jsp就能看到所有资源的统计结果,而且不会再出现循环调用。
方案2:调整Servlet逻辑并将JSP放入WEB-INF
如果你不想改用Filter,可以通过修改Servlet逻辑+WEB-INF目录的特性来避免循环:
步骤1:移动JSP到WEB-INF目录
把FullTracking.jsp移动到项目的WEB-INF文件夹下,路径变为WEB-INF/FullTracking.jsp(WEB-INF下的资源无法通过外部URL直接访问,但服务器内部转发可以访问)。
步骤2:修改Servlet代码
在doGet方法开头添加判断,跳过WEB-INF下的资源处理,同时修改转发路径:
public class FullTracking extends HttpServlet { Hashtable<String,Integer> links=new Hashtable<>(); public void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException{ String uri=req.getRequestURI(); String resource=uri.substring(req.getContextPath().length()); // 跳过WEB-INF下的资源,直接交给默认Servlet/JSP处理 if (resource.startsWith("/WEB-INF/")) { req.getRequestDispatcher(resource).forward(req, res); return; } // 原统计逻辑 if(links.containsKey(resource)){ Integer count=links.get(resource); links.put(resource,new Integer(1+count.intValue())); } else{ links.put(resource,new Integer(1)); } req.setAttribute("links",links); // 转发到WEB-INF下的JSP req.getRequestDispatcher("/WEB-INF/FullTracking.jsp").forward(req,res); } public void doPost(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException{ doGet(req,res); } }
步骤3:保留原Servlet映射
继续使用<url-pattern>/*</url-pattern>映射,此时访问任何资源都会被统计,转发到WEB-INF下的JSP时,Servlet会直接放行,不会触发循环。
方案3:在Servlet中跳过统计页面的处理
这个方案无需移动文件,但需要在Servlet中判断当前请求是否为统计页面,直接放行:
public void doGet(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException{ String uri=req.getRequestURI(); String resource=uri.substring(req.getContextPath().length()); // 如果是统计页面,直接交给JSP Servlet处理,跳过统计逻辑 if ("/FullTracking.jsp".equals(resource)) { req.getRequestDispatcher(resource).forward(req, res); return; } // 原统计逻辑 if(links.containsKey(resource)){ Integer count=links.get(resource); links.put(resource,new Integer(1+count.intValue())); } else{ links.put(resource,new Integer(1)); } req.setAttribute("links",links); req.getRequestDispatcher("/FullTracking.jsp").forward(req,res); }
不过这个方案有个小问题:如果用户直接访问/FullTracking.jsp,不会被统计(这其实是合理的),但转发时的请求会被Servlet拦截一次,不过因为判断后直接放行,不会进入统计和再次转发,所以不会循环。
另外还要提醒你:你的Hashtable虽然是线程安全的,但在多线程环境下,get+put的操作不是原子性的,建议在统计逻辑上加synchronized块,避免计数错误(方案1中已经添加了同步处理)。
内容的提问来源于stack exchange,提问作者1m9K

