SpringBoot中给Tomcat添加过滤器

发布时间 2023-10-22 11:52:09作者: 写的代码很烂

SpringBoot中给Tomcat添加过滤器

一、引入

JavaWeb组件Servlet提供了filter过滤功能,其功能是对目标资源的请求和响应进行拦截,对拦截到的请求和响应做出特殊的功能处理,比如我们请求中有一些敏感信息过滤就是利用过滤器过滤

二、Filter功能概述

Java Servlet API中提供了Filter接口,编写Filter的实现类,从而实现自定义过滤器。

Filter的请求流程为:

  • 1、客户端发起请求服务容器判断当前请求资源是否有过滤器,

  • 2、有则执行过滤器过滤器过滤通过后请求到Servlet服务器返回结果通过过滤器返回给请求方

Filter接口源码:

package javax.servlet;

import java.io.IOException;

public interface Filter {
    
    // 初始化方法
    default void init(FilterConfig filterConfig) throws ServletException {
    }
    
	// 过滤方法
    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
    
    // 初始化方法   
    default void destroy() {
    }
}
init() 此方法在只在过滤器创建的时候执行一次,用于初始化过滤器的属性
doFilter() 该方法会对请求进行拦截,用户需要在该方法中自定义对请求内容以及响应内容进行过滤的,调用该方法的入参 FilterChain对象的 doFilter 方法对请求放行执行后面的逻辑,若未调用 doFilter 方法则本次请求结束,并向客户端返回响应失败
destroy() 此方法用于销毁过滤器,过滤器被创建以后只要项目一直运行,过滤器就会一直存在,在项目停止时,会调用该方法销毁过滤器

三、添加过滤器进行实操

在SpringBoot中有两种方式实现自定义Filter:

  • 1、使用 @WebFilter 和 @ServletComponentScan 组合注解;
  • 2、通过配置类注入 FilterRegistrationBean对象

3.1、注解版

注解版是最常用的了。首先看下 @WebFilter注解中的信息:

@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;
}

几个重要参数详解:

属性 功能描述
urlPatterns 自定义需要拦截的URL,可以使用正则匹配,若没指定该参数值,则默认拦截所有请求
filterName 自定义过滤器的名称
initParams 自定义过滤器初始化参数的数组,此参数可以通过自定义过滤器 init() 的入参FilterConfig对象的 getInitParameter() 方法获取;(由于过滤器没有直接排除自定义URL不拦截的设定,如果我们需要在自定义拦截的URL中排除部分不需要拦截的URL,可以通过将需要排除的URL放到initParams参数中再在doFilter方法中排除)

如下自定义个一个拦截所有URL除了带有 “/test” 的片段名称为testFilter的过滤器:

@WebFilter(filterName = "testFilter", urlPatterns = "/*", 
        initParams = @WebInitParam(name = "noFilterUrl", value = "/test"))
public class TestFilter implements Filter {
    private List<String> noFilterUrls; 
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 从过滤器配置中获取initParams参数
        String noFilterUrl = filterConfig.getInitParameter("noFilterUrl");
        // 将排除的URL放入成员变量noFilterUrls中
        if (StringUtils.isNotBlank(noFilterUrl)) {
            noFilterUrls = new ArrayList<>(Arrays.asList(noFilterUrl.split(",")));
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        // 若请求中包含noFilterUrls中的片段则直接跳过过滤器进入下一步请求中
        String url = ((HttpServletRequest)servletRequest).getRequestURI();
        Boolean flag = false;
        if (!CollectionUtils.isEmpty(noFilterUrls)) {
            for (String noFilterUrl : noFilterUrls) {
                if (url.contains(noFilterUrl)) {
                    flag = true;
                    break;
                }
            }
        }
        if (!flag) {
            ......    //过滤请求响应逻辑
        } 
        // 一定要写上,不然后续的过滤器链条无法进行调用
        // 具体源码参考:org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

在启动类上需要添加@ServletComponentScan注解才能使过滤器生效

@SpringBootApplication
@ServletComponentScan(basePackages = "com.guang.springbootfilter.filter")
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class);
    }
}

这里需要注意的是,如果实现多个FIlter功能的过滤器。使用@WebFilter注解的方式只能根据过滤器名的类名顺序执行,添加@Order注解是无效的,因为@WebFilter在容器加载时,不会使用@Order注解定义的顺序,而是默认直接使用类名排序。所以使用这种方式实现多个过滤器,且有顺序要求,则需要注意类名的定义。

但是一个项目中也不会存在着多个过滤器,要是存在着多个过滤器的话,可以考虑下是否可以使用拦截器来进行调用?

如果真的需要考虑到使用多个过滤器,那么参考如下所示:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * @Description
 * @Author liguang
 * @Date 2023/10/22/09:23
 */
@WebFilter(urlPatterns = "/*",filterName = "a")
public class AFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(AFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("进入过滤器,但是不做拦截---------------------a");
        chain.doFilter(request,response);
    }
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * @Description
 * @Author liguang
 * @Date 2023/10/22/09:23
 */
@WebFilter(urlPatterns = "/*",filterName = "b")
public class BFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(BFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("进入过滤器,但是不做拦截-----------------b");
        chain.doFilter(request,response);
    }
}

那么看一下打印结果:

进入过滤器,但是不做拦截-----------------a
进入过滤器,但是不做拦截-----------------b

3.2、配置版本

配置版本这里就不再演示了。直接放上参考链接:

1、https://www.cnblogs.com/cy0628/p/16379612.html

2、https://www.cnblogs.com/longan-wang/p/15527516.html

四、原理探究

下面以注解版本为例,来进行探究。因为@WebFilter是Tomcat提供的,而@ServletComponentScan注解是Springboot提供的。

4.1、解析过程

所以我最开始的猜测就是因为是@ServletComponentScan注解发挥了作用,因为会去扫描指定包下添加了@WebFilter注解的类,检查其是否实现了javax.servlet.Filter接口。如果符合条件,那么会调用context添加到过滤器链条中去的。

那么带着这个思路来进行验证一下:

@Import(ServletComponentScanRegistrar.class)
public @interface ServletComponentScan {}

看到这里已经很明显了,添加了一个Bean来进行处理

package org.springframework.boot.web.servlet;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;

/**
 * {@link ImportBeanDefinitionRegistrar} used by
 * {@link ServletComponentScan @ServletComponentScan}.
 *
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 */
class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar {

	private static final String BEAN_NAME = "servletComponentRegisteringPostProcessor";

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
         // 获取得到扫描的包路径
		Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
        // 判断容器中是否存在这个名称的BD,一般来说,不应该存在的
		if (registry.containsBeanDefinition(BEAN_NAME)) {
			updatePostProcessor(registry, packagesToScan);
		}
		else {
            // 直接看到这里
			addPostProcessor(registry, packagesToScan);
		}
	}

	private void updatePostProcessor(BeanDefinitionRegistry registry, Set<String> packagesToScan) {
		BeanDefinition definition = registry.getBeanDefinition(BEAN_NAME);
		ValueHolder constructorArguments = definition.getConstructorArgumentValues().getGenericArgumentValue(Set.class);
		@SuppressWarnings("unchecked")
		Set<String> mergedPackages = (Set<String>) constructorArguments.getValue();
		mergedPackages.addAll(packagesToScan);
		constructorArguments.setValue(mergedPackages);
	}

	private void addPostProcessor(BeanDefinitionRegistry registry, Set<String> packagesToScan) {
        // 添加了ServletComponentRegisteringPostProcessor类型的Bean
		GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
		beanDefinition.setBeanClass(ServletComponentRegisteringPostProcessor.class);
		beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packagesToScan);
		beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
		registry.registerBeanDefinition(BEAN_NAME, beanDefinition);
	}

	private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
		AnnotationAttributes attributes = AnnotationAttributes
				.fromMap(metadata.getAnnotationAttributes(ServletComponentScan.class.getName()));
		String[] basePackages = attributes.getStringArray("basePackages");
		Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
		Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
		for (Class<?> basePackageClass : basePackageClasses) {
			packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
		}
		if (packagesToScan.isEmpty()) {
			packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
		}
		return packagesToScan;
	}

}

那么重点就是看一下ServletComponentRegisteringPostProcessor这个bean,看起来像是一个BeanFactoryPostProcessor,点进去发现果然是。那么重点直接来到了postProcessBeanFactory方法了

	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
         // 此时此刻还没有servletcontext
		if (isRunningInEmbeddedWebServer()) {
             // 这个类看下来像是完成指定包下是否存在继承Filter接口并添加@WebFilter注解的类
			ClassPathScanningCandidateComponentProvider componentProvider = createComponentProvider();
			for (String packageToScan : this.packagesToScan) {
				scanPackage(componentProvider, packageToScan);
			}
		}
	}

看一下创建方法

	private static final List<ServletComponentHandler> HANDLERS;

	static {
         // 添加各种类型的处理器
		List<ServletComponentHandler> servletComponentHandlers = new ArrayList<>();
		servletComponentHandlers.add(new WebServletHandler());
		servletComponentHandlers.add(new WebFilterHandler());
		servletComponentHandlers.add(new WebListenerHandler());
		HANDLERS = Collections.unmodifiableList(servletComponentHandlers);
	}



	private ClassPathScanningCandidateComponentProvider createComponentProvider() {
		ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(false);
		componentProvider.setEnvironment(this.applicationContext.getEnvironment());
		componentProvider.setResourceLoader(this.applicationContext);
        // 开始进行添加
		for (ServletComponentHandler handler : HANDLERS) {
			componentProvider.addIncludeFilter(handler.getTypeFilter());
		}
		return componentProvider;
	}
	private void scanPackage(ClassPathScanningCandidateComponentProvider componentProvider, String packageToScan) {
		for (BeanDefinition candidate : componentProvider.findCandidateComponents(packageToScan)) {
			if (candidate instanceof AnnotatedBeanDefinition) {
				for (ServletComponentHandler handler : HANDLERS) {
                      // 获取得到各种handler来进行处理
					handler.handle(((AnnotatedBeanDefinition) candidate),
							(BeanDefinitionRegistry) this.applicationContext);
				}
			}
		}
	}

下面看一下具体的过程:

	void handle(AnnotatedBeanDefinition beanDefinition, BeanDefinitionRegistry registry) {
        // 获取得到各种属性来进行遍历
		Map<String, Object> attributes = beanDefinition.getMetadata()
				.getAnnotationAttributes(this.annotationType.getName());
		if (attributes != null) {
             // 不同的实现类可以来进行扫描处理
			doHandle(attributes, beanDefinition, registry);
		}
	}

因为我们当前处理的是@WebFilter,所以看下org.springframework.boot.web.servlet.WebFilterHandler#doHandle中的处理过程

	public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
			BeanDefinitionRegistry registry) {
         // 直接创建了FilterRegistrationBean类型的bean
		BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterRegistrationBean.class);
		builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
		builder.addPropertyValue("dispatcherTypes", extractDispatcherTypes(attributes));
		builder.addPropertyValue("filter", beanDefinition);
		builder.addPropertyValue("initParameters", extractInitParameters(attributes));
        // 获取得到fitler的名称
		String name = determineName(attributes, beanDefinition);
		builder.addPropertyValue("name", name);
		builder.addPropertyValue("servletNames", attributes.get("servletNames"));
		builder.addPropertyValue("urlPatterns", extractUrlPatterns(attributes));
		registry.registerBeanDefinition(name, builder.getBeanDefinition());
	}

4.2、如何添加到ServletContext中?

既然已经创建好啦fitler,那么如何添加到serlvet上下文中的呢?

那么直接在BFilter中添加一个构造方法

@WebFilter(urlPatterns = "/*",filterName = "b")
public class BFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(BFilter.class);
    public BFilter() {
        logger.info("实例化filter");
    }



    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("进入过滤器,但是不做拦截-----------------b");
        chain.doFilter(request,response);
    }
}

打上断点来进行观察,最终发现是通过:

	private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
		return this::selfInitialize;
	}

	private void selfInitialize(ServletContext servletContext) throws ServletException {
		prepareWebApplicationContext(servletContext);
		registerApplicationScope(servletContext);
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
            // 这里调过去的
			beans.onStartup(servletContext);
		}
	}

具体看一下:

	public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
			Class<? extends ServletContextInitializer>... initializerTypes) {
		this.initializers = new LinkedMultiValueMap<>();
		this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
				: Collections.singletonList(ServletContextInitializer.class);
        // 通过这里来进行进行调用的
		addServletContextInitializerBeans(beanFactory);
		addAdaptableBeans(beanFactory);
		List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
				.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
				.collect(Collectors.toList());
		this.sortedList = Collections.unmodifiableList(sortedInitializers);
		logMappings(this.initializers);
	}

进一步来看:

private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
        for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
                                                                                                        initializerType)) {
            addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
        }
    }
}

直接来到重点:

	private <T> List<Entry<String, T>> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class<T> type,
			Set<?> excludes) {
		String[] names = beanFactory.getBeanNamesForType(type, true, false);
		Map<String, T> map = new LinkedHashMap<>();
		for (String name : names) {
			if (!excludes.contains(name) && !ScopedProxyUtils.isScopedTarget(name)) {
				T bean = beanFactory.getBean(name, type);
				if (!excludes.contains(bean)) {
					map.put(name, bean);
				}
			}
		}
         // 这里有@Order的排序
		List<Entry<String, T>> beans = new ArrayList<>(map.entrySet());
		beans.sort((o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getValue(), o2.getValue()));
		return beans;
	}

关键是这里的type为ServletContextInitializer,而我们的filter最终注册成的类型是FilterRegistrationBean,这里的FilterRegistrationBean是ServletContextInitializer的实现类,而DispatcherServletRegistrationBean也是其实现之一,所以接下来就应该是进行类型排除。

那么重点在于就在于seen和initializers这两个集合中的一个如何添加到servletcontext中去的?

那么就直接看一下ServletContextInitializer中的onStartup方法

	public final void onStartup(ServletContext servletContext) throws ServletException {
		String description = getDescription();
		if (!isEnabled()) {
			logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
			return;
		}
        // 看着非常像
		register(description, servletContext);
	}

最终跟踪进去,发现以下步骤:

	protected Dynamic addRegistration(String description, ServletContext servletContext) {
		Filter filter = getFilter();
		return servletContext.addFilter(getOrDeduceName(filter), filter);
	}

至此,追踪结束

五、总结

1、先是概述了Filter的作用;

2、两种方式配置Filter:①注解版;②配置版;

3、分析了Tomcat中的servletcontext中是如何添加filter的源码;