request.getRequestDispatcher().forward()的妙用以及DispatcherType 对Filter配置的影响

19 篇文章 1 订阅
5 篇文章 0 订阅

背景

在这里插入图片描述
我们应用如上图所示,Nignx做负债均衡,微服务间使用feign进行调用。
为了方便鉴权Filter配置拦截的url以及nginx配置对外暴露的url,我们为所有服务设计了统一的url规范

类型用途
v1/xx给前端用的url
v5/xx内部接口,服务间调用

因此所有服务都未配置server.servlet.context-path
那么问题来了,现在我们要把服务从虚拟机迁移到docker中。使用公司的docker需要有用于分发的文根,因为docker服务提供了公共域名(减小各个产品各自去申请域名的工作量),这样url必须有前缀文根让公共域名知道请求往哪个应用分发。同时docker的ip是变化的。配置nignx的upstream时只能配置域名,不能在像之前配置虚拟机的ip。
在这里插入图片描述
为了降低改动量,因此在保留原有的url外,重新提供一套带文根的url。–这样既能保证nginx能够方便的使用域名来配置upstream,也能保证之前feign调用的url保持不变。
另外为了保证之前配置的Filter对新url生效,请求进来后需要重定向到老url上。
备注上述所说的文根并非指servlet中的context-path,context-path只是servlet中的概念,对于HTTP或者nginx来讲,他们是没有context-path的概念的,他们只是需要利用url中的一小节进行路由分发。因此我们在v1/xx的基础上增加/a/v1/xx的url即可,至于是通过配置context-path实现,还是通过配置servlet-path实现都是可以的。

工作一-新增一套带文根的url

新增一套带文根的url,同时又保留老的url,只能给spring的DispatcherServlet新增一个servlet-path,类似于用原生servlet开发应用时,给一个servlet配置多个url。

<servlet-mapping>
    <servlet-name>RedServlet</servlet-name>
    <url-pattern>/red/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>RedServlet</servlet-name>
    <url-pattern>/red/red/*</url-pattern>
</servlet-mapping>

配置方法如下,因为新url和老url都要走到同一个业务类中,所以得复用spring自己自动配置的DispatcherServlet。spring自动配置的DispatcherServlet见org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration

@Bean
    public ServletRegistrationBean dispatcherServletWithNewPrefix(DispatcherServlet dispatcherServlet) {
        ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet,
                "/ceshi/*");
        registration.setName("ceshi");
        return registration;
    }

闭坑指导一

如上代码配置ServletRegistrationBean时,一定重新配置Name。因为tomcat在配置servlet的时候会根据servletName进行去重,如果有同名的servlet,后面的会注册失败。
详细的可以跟踪org.springframework.boot.web.servlet.ServletRegistrationBean#addRegistration断点往下看,这里重点关注对应的逻辑org.apache.catalina.core.ApplicationContext#addServlet(java.lang.String, java.lang.String, javax.servlet.Servlet, java.util.Map<java.lang.String,java.lang.String>)。

    private ServletRegistration.Dynamic addServlet(String servletName, String servletClass,
            Servlet servlet, Map<String,String> initParams) throws IllegalStateException {
        # 此处参数校验等不重要的逻辑
        
        # 通过本次要注册的servletName获取之前注册过的
        Wrapper wrapper = (Wrapper) context.findChild(servletName);


        if (wrapper == null) {
            wrapper = context.createWrapper();
            wrapper.setName(servletName);
            context.addChild(wrapper);
        } else {
            # wrapper不为空说明之前有同名servlet,此次的servlet不在进行注册
            if (wrapper.getName() != null &&
                    wrapper.getServletClass() != null) {
                if (wrapper.isOverridable()) {
                    wrapper.setOverridable(false);
                } else {
                    return null;
                }
            }
        }
        ... ...
 }

闭坑指导二

如果按照上述配置代码,仅仅只是给dispatcherServlet配置新的url及name,会导致上传附件功能异常。

 @RequestMapping(value = "/xx/upload", method = RequestMethod.POST)
 public handleFormUpload(@RequestParam("file") MultipartFile file)

上述接口在处理附件时会抛出以下异常

o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.IllegalStateException: Unable to process parts as no multi-part configuration has been provided
	at org.apache.catalina.connector.Request.parseParts(Request.java:2866)
	at org.apache.catalina.connector.Request.getParts(Request.java:2834)
	at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098)
	at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
	at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
	at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
	at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
	at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95)
	at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88)
	at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122)
	at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1205)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
	... ...

跟踪堆栈,结合相关分析可得到如下信息:

  1. spring mvc为了简化附件上传,在进入业务处理前,对先对附件进解析,即堆栈中的
    org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1205)
    解析后将HttpServletRequest转换成MultipartHttpServletRequest,方便上层使用,获取附件。
  2. serlvet3.0开始,HttpServletRequest接口新增了getPart接口,用于方便的处理附件上传(multipart/form-data类型的请求)。
    public Collection<Part> getParts() throws IOException, ServletException;
    public Part getPart(String name) throws IOException, ServletException;

新版本的spring mvc解析附件时,不在通过commons-fileupload进行处理,而是直接使用的servlet原生的api。
追踪堆栈可以看到如下代码
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95)

	private void parseRequest(HttpServletRequest request) {
		try {
		    # 使用servlet原生的API对附件进行处理
			Collection<Part> parts = request.getParts();
			this.multipartParameterNames = new LinkedHashSet<>(parts.size());
			MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
			for (Part part : parts) {
				... 
			}
			setMultipartFiles(files);
		}
	}
  1. 跟踪堆栈,可以定位到追踪抛出异常代码的位置
    private void parseParts(boolean explicit) {
        ... 
        Context context = getContext();
        MultipartConfigElement mce = getWrapper().getMultipartConfigElement();

        if (mce == null) {
            if(context.getAllowCasualMultipartParsing()) {
                mce = new MultipartConfigElement(null, connector.getMaxPostSize(),
                        connector.getMaxPostSize(), connector.getMaxPostSize());
            } else {
                if (explicit) {
                    # 异常在这里抛出
                    partsParseException = new IllegalStateException(
                            sm.getString("coyoteRequest.noMultipartConfig"));
                    return;
                } else {
                    parts = Collections.emptyList();
                    return;
                }
            }
    }

对比没改造之前,发现就是因为mce为空导致。经过调试及查阅相关信息可知
MultipartConfigElement mce = getWrapper().getMultipartConfigElement(),这一行是获取servlet上传附件的配置。默认情况下(allowCasualMultipartParsing=false),如果不配置multipartConfig的情况下使用getPart接口会抛异常。
如何配置multipartConfig,可见spring boot对dispatch的自动配置。
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration

		public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
				WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
			DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
					webMvcProperties.getServlet().getPath());
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
			# 对servlet的MultipartConfig进行配置
			multipartConfig.ifAvailable(registration::setMultipartConfig);
			return registration;
		}

当然我们也可以配置全局开关allowCasualMultipartParsing,详见百度。

工作二-访问新url时,内部重定向到老url上

@WebFilter("/ceshi/*")
public class TestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String dispatcher = httpServletRequest.getPathInfo();
        request.getRequestDispatcher(dispatcher).forward(request, response);
    }
}

闭坑指导一

Filter里面不能在调用chain.doFilter(request,response)
chain代表本次请求的执行流程,里面包含了需要执行的Filter以及servlet,如果执行完forward后再调用chain.doFilter,会将本该丢弃的流程重新执行一遍。
在这里插入图片描述

闭坑指导二-Filter的类型

forward后会调用那些Filter,是之前流程中还没调用的Filter吗?
要回答这个问题需要跟踪forward后的源码。具体逻辑见org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
调用栈如下
在这里插入图片描述

    public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {
        # 去掉多余的参数校验,保留主逻辑方便代码阅读
        
        ApplicationFilterChain filterChain = new ApplicationFilterChain();
        
        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();

        # 重定向后DispatcherType为FORWARD
        DispatcherType dispatcher =
                (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

        String requestPath = null;
        Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
        if (attribute != null){
            requestPath = attribute.toString();
        }

        String servletName = wrapper.getName();

        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            # 判断FilterDispatcherType是否匹配
            if (!matchDispatcher(filterMap, dispatcher)) {
                continue;
            }
            # 判断Filter的url是否匹配
            if (!matchFiltersURL(filterMap, requestPath))
                continue;
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            filterChain.addFilter(filterConfig);
        }

        // Add filters that match on servlet name second
        for (FilterMap filterMap : filterMaps) {
            if (!matchDispatcher(filterMap, dispatcher)) {
                continue;
            }
            # 判断Filter是否匹配该servletName
            if (!matchFiltersServlet(filterMap, servletName))
                continue;
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            filterChain.addFilter(filterConfig);
        }
        return filterChain;
    }
    private static boolean matchDispatcher(FilterMap filterMap, DispatcherType type) {
        switch (type) {
            case FORWARD :
                if ((filterMap.getDispatcherMapping() & FilterMap.FORWARD) != 0) {
                    return true;
                }
                break;
            case INCLUDE :
                if ((filterMap.getDispatcherMapping() & FilterMap.INCLUDE) != 0) {
                    return true;
                }
                break;
            case REQUEST :
                if ((filterMap.getDispatcherMapping() & FilterMap.REQUEST) != 0) {
                    return true;
                }
                break;
            case ERROR :
                if ((filterMap.getDispatcherMapping() & FilterMap.ERROR) != 0) {
                    return true;
                }
                break;
            case ASYNC :
                if ((filterMap.getDispatcherMapping() & FilterMap.ASYNC) != 0) {
                    return true;
                }
                break;
        }
        return false;
    }

由上可见,不管是正常的处理,还是从定向后的处理,筛选Filter时都遵从统一的逻辑,即DispatcherType是否满足、path是否满足、servlet是否满足。

ps:看来国外小哥写代码也不咋滴啊,上面两个循环命名可以合并成一个的。不仅如此,上述代码还会导致同时满足path和servletName的filter会重复添加

for (FilterMap filterMap : filterMaps) {
    if (!matchDispatcher(filterMap, dispatcher)) {
        continue;
    }
    if (!matchFiltersURL(filterMap, requestPath) && !matchFiltersServlet(filterMap, servletName))
        continue;
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
    filterChain.addFilter(filterConfig);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这段代码是一个 Java 代码块,实现的是会员登录功能。下面是每一行代码的解释: 1. `private void huiyuanLogin(HttpServletRequest request, HttpServletResponse response)`:定义了一个名为 `huiyuanLogin` 的方法,其中 `HttpServletRequest` 和 `HttpServletResponse` 分别表示 HTTP 请求和响应。 2. `String accountname = request.getParameter("accountname");`:从 HTTP 请求中获取名为 `accountname` 的参数,并将其赋值给 `accountname` 变量。 3. `String password = request.getParameter("password");`:从 HTTP 请求中获取名为 `password` 的参数,并将其赋值给 `password` 变量。 4. `String filter = MessageFormat.format("where accountname=''{0}'' and password=''{1}''", accountname, password);`:使用 `MessageFormat` 类将 `accountname` 和 `password` 拼接成一个 SQL 查询语句,并将其赋值给 `filter` 变量。 5. `Huiyuan huiyuan = (Huiyuan) DALBase.load("huiyuan", filter);`:使用 `DALBase` 类从数据库中加载会员信息,其中 `huiyuan` 表示表名,`filter` 表示查询条件,将查询结果转换为 `Huiyuan` 类型,并将其赋值给 `huiyuan` 变量。 6. `String errorurl=request.getParameter("errorurl");`:从 HTTP 请求中获取名为 `errorurl` 的参数,并将其赋值给 `errorurl` 变量。 7. `String forwardurl=request.getParameter("forwardurl");`:从 HTTP 请求中获取名为 `forwardurl` 的参数,并将其赋值给 `forwardurl` 变量。 8. `if (huiyuan != null && huiyuan.getPassword().equals(password)) {`:如果 `huiyuan` 不为 null,且 `huiyuan` 的密码与输入的密码相同,则执行下面的代码块。 9. `huiyuan.setLogtimes(huiyuan.getLogtimes() + 1);`:将 `huiyuan` 的登录次数加 1。 10. `DALBase.update(huiyuan);`:使用 `DALBase` 类将更新后的 `huiyuan` 对象保存回数据库中。 11. `request.getSession().setAttribute("huiyuan", huiyuan);`:将 `huiyuan` 对象存储到 HTTP 会话中,以便后续使用。 12. `if (forwardurl != "") response.sendRedirect(SystemParam.getSiteRoot() + forwardurl);`:如果 `forwardurl` 不为空,则将用户重定向到 `forwardurl` 指定的页面。 13. `else response.sendRedirect(SystemParam.getSiteRoot() + "/e/huiyuan/accounintfo.jsp");`:否则将用户重定向到默认页面 `/e/huiyuan/accounintfo.jsp`。 14. `} else {`:如果 `huiyuan` 为 null,或者输入的密码与 `huiyuan` 的密码不相同,则执行下面的代码块。 15. `dispatchParams(request, response);`:将 HTTP 请求转发给其他页面进行处理。 16. `request.setAttribute("errormsg", "<label class='error'>系统账户和密码不匹配<label>");`:设置一个名为 `errormsg` 的请求属性,并将其值设置为错误信息。 17. `request.getRequestDispatcher(errorurl).forward(request, response);`:将 HTTP 请求转发到 `errorurl` 指定的页面,并将响应返回给客户端。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值