问题背景

最近在开发项目接口,基于SpringBoot 2.6.8,最终部署到外置Tomcat 8.5.85 下,开发过程中写了一个CookieFilter,实现javax.servlet.Filter接口,代码编译期正常。部署到外置Tomcat 8.5.85 下,在控制台上报错:

16-Jan-2023 16:11:07.756 严重 [localhost-startStop-1] org.apache.catalina.core.StandardContext.filterStart 启动过滤器异常[cookieFilter]
	java.lang.AbstractMethodError
		at org.apache.catalina.core.ApplicationFilterConfig.initFilter(ApplicationFilterConfig.java:281)
		at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:109)
		at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4604)
		...省略其他输出...

日志截图如下:

【问题解决】Tomcat启动服务时提示Filter初始化或销毁出现java.lang.AbstractMethodError错误-LMLPHP

除了初始化错误还有销毁错误,错误类型与以下的错误类型一致:

16-Jan-2023 16:11:07.876 严重 [localhost-startStop-1] org.apache.catalina.core.ApplicationFilterConfig.release 失败的销毁过滤器类型为[xx.CookieFilter]名称为[CookieFilter]
	java.lang.AbstractMethodError
		at org.apache.catalina.core.ApplicationFilterConfig.release(ApplicationFilterConfig.java:312)
		at org.apache.catalina.core.StandardContext.filterStop(StandardContext.java:4638)
		...省略其他输出...

日志截图如下:

【问题解决】Tomcat启动服务时提示Filter初始化或销毁出现java.lang.AbstractMethodError错误-LMLPHP

我的代码差不多长这样:

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class CookieFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
		//做一些处理...
        filterChain.doFilter(request, response);
    }

}

反复分析

  • 首先要知道这个错误(java.lang.AbstractMethodError)是什么?
    • 【问题解决】Tomcat启动服务时提示Filter初始化或销毁出现java.lang.AbstractMethodError错误-LMLPHP
    • 也就是说父子两个类编译时机不同,先编译父类,在编译子类前对父子类方法定义做了不兼容的修改,编译子类通过后,让子类与先编译的父类一起运行,导致运行期实例化子类时找不到父类定义抽象方法的实现,从而抛出AbstractMethodError错误。
  • 根据Filter的生命周期,初始化init,运行期doFilter,销毁destroy,我们可以推测出是CookieFilter没有实现init方法与destroy方法
  • 这里问题就来了:这里的父(接口)是javax.servlet.Filter,实现的CookieFilter只是一个Filter实现类,我们并没有办法修改Filter接口,按理说编译期就应该报错才对嘛,为啥能编译成功呢?
    • 原因就在于在SpringBoot上使用Filter接口需要引用javax.servlet:javax.servlet-api依赖包,这个包里定义的Filter接口的init()与destroy()是有默认实现的,代码如图:
    • 【问题解决】Tomcat启动服务时提示Filter初始化或销毁出现java.lang.AbstractMethodError错误-LMLPHP
    • 也就是说,我们编译期不重写init()与destroy()其实是可以的;
  • 但是运行在外置Tomcat8.5.x上的时候,这两个方法却要求必须实现了么?
    • 有两种可能,一是Tomcat 8.5.x的共享库中有Filter定义,与我们需要的Filter不同,没有默认实现init与destroy方法;另一个可能是我们打的包中有两个包包含javax.servlet.Filter,运行期JVM加载顺序不一致就会引出不同的问题!
  • 根据上边的猜测一,我找到了Tomcat 8.5.85的Filter源码https://github.com/apache/tomcat/blob/8.5.85/java/javax/servlet/Filter.java,我们发现在第67行init方法的确没有default关键字修饰,destroy方法也是这样的,第一个猜测是成立的
  • 根据猜测二,我在程序War包中找到了两个servlet-api包:javax.servlet-api与servlet-api,前者版本较后者新。问题分析到这里就可以做解决方案了。

解决方案

  • 方案一:不管新旧servlet-api包,所有Filter都添加默认init与destroy方法
  • 方案二:升级外置Tomcat版本到9.x,原因是9.x的Tomcat的共享库Filter有默认实现init与destroy方法
  • 方案三:构建排除较新的javax.servlet-api包,继续使用Tomcat 8.5.x,同样地所有Filter都要添加init与destroy方法,空的方法也可以。
  • 方案四:使用内嵌Tomcat9.x部署,构建排除旧版servlet-api包
  • 方案五:构建排除javax.servlet-api包与旧版servlet-api包,代码改造添加init与destroy方法,以共享库定义Filter为主
  • 方案六:不大推荐。构建排除旧版servlet-api包,仍部署在Tomcat 8.5.x,有可能会加载到共享库里的Filter

这几种方案中对于研发层面最简单避免这个问题的就是方案一,这里的解决方案是抛砖引玉,欢迎大家评论给出更优解。我是Hellxz,下次博客见。

01-17 09:41