You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot集成测试中无法注入自定义Filter的问题

问题:Spring集成测试中无法实例化自定义Filter导致测试失败

我在Spring控制器中定义了一个POST方法,尝试用TestRestTemplate的exchange方法做集成测试,但运行测试时无法实例化自定义Filter,导致测试失败。

控制器代码(Controller.java)

@RequestMapping(path = "/rest/projects")
@RestController
public class Controller {
    public static final Logger LOGGER = LoggerFactory.getLogger(Controller.class);
    private JwtTokenProvider jwtTokenProvider;
    private MailsService mailsService;

    @Autowired
    public Controller(MailsService mailsService, JwtTokenProvider jwtTokenProvider) {
        this.mailsService = mailsService;
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @PostMapping(value = "/{projectId}/mails/filter/{type}")
    public ListDto getMails(@PathVariable("type") String mailType, @PathVariable("projectId") String projectId,
                           @RequestParam(value = "page_number", required = false) String pageNumber,
                           @RequestParam(value = "page_size", required = false) String pageSize,
                           @RequestParam(value = "search_query", required = false) String searchKeyword,
                           @RequestParam(value = "sort", required = false) String sortBy,
                           @RequestParam(value = "sort_direction", required = false) String sortDirection,
                           @RequestBody MailSearchModel mailSearchModel) throws BackendError {
        //调用服务
    }
}

测试类代码(ControllerTest.java)

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ControllerTest {

    @Autowired
    private MockMvc mock;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private RequestHeaderFilter requestHeaderFilter;

    @LocalServerPort
    private int port;

    TestRestTemplate restTemplate = new TestRestTemplate();

    HttpHeaders headers = new HttpHeaders();

    @Before
    public void setUp() {
        mock = MockMvcBuilders.webAppContextSetup(wac)
                .addFilter(requestHeaderFilter).build();
    }


    @Test
    public void singleFieldProjectFields() throws Exception {

        ObjectMapper mapper = new ObjectMapper();
        MailSearchModel mailSearchModel = mapper.readValue(new File("./src/test/resources/SingleValueSchema.json"),
                MailSearchModel.class);
        LinkedMultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
        requestParams.add("sort_direction", "DESC");
        requestParams.add("page_number", "1");
        requestParams.add("sort", "sentdate");
        requestParams.add("page_size", "100");

        HttpEntity<MailSearchModel> entity = new HttpEntity<MailSearchModel>(mailSearchModel, headers);
        ResponseEntity<String> response = restTemplate.exchange(
                createURLWithPort("/rest/projects/{projectId}/mails/filter/{type}"), HttpMethod.POST, entity,
                String.class,"1879053221","inbox",requestParams);

        String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);

        assertTrue(actual.contains("/mails"));
    }
}

自定义Filter代码(RequestHeaderFilter.java)

@WebFilter(urlPatterns = "/rest/*")
@Order(1)
public class RequestHeaderFilter implements Filter {
    private LocalizationHelper localizationHelper;
    private ObjectMapper jacksonObjectMapper;
    private JwtTokenProvider jwtTokenProvider;
    private BabylonCookieProvider babylonCookieProvider;
    private BabylonSessionService babylonSessionService;

    @Autowired
    public RequestHeaderFilter(LocalizationHelper localizationHelper, ObjectMapper jacksonObjectMapper,
                               JwtTokenProvider jwtTokenProvider, BabylonCookieProvider babylonCookieProvider,
                               BabylonSessionService babylonSessionService) {
        this.localizationHelper = localizationHelper;
        this.jacksonObjectMapper = jacksonObjectMapper;
        this.jwtTokenProvider = jwtTokenProvider;
        this.babylonCookieProvider = babylonCookieProvider;
        this.babylonSessionService = babylonSessionService;
    }

    @Override
    public void init(FilterConfig filterConfig) {
        // 无操作
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 在请求作用域Bean中添加RequestId
        addRequestIdInSessionScope(httpRequest);

        // 检查请求认证
        String auth = httpRequest.getHeader("Authorization");
        if (Utility.isNullOrEmpty(auth)) {
            ErrorDto errorDto = getErrorDto(HttpStatus.BAD_REQUEST, JwtConstants.TOKEN_MISSING);
            postErrorResponse(httpResponse, errorDto);
        } else {
            auth = auth.replace(JwtConstants.TOKEN_TYPE_BEARER, "").trim();
            String tokenStatus = parseJweToken(auth);
            afterValidation(request, response, chain, auth, tokenStatus);
        }
    }
}

主类代码(MobileApplication.java)

@SpringBootApplication
@ServletComponentScan
public class MobileApplication {
    public static void main(String[] args) {
        SpringApplication.run(MobileApplication.class, args);
    }
}

错误日志

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.oracle.cegbu.aconex.filter.RequestHeaderFilter': Cannot create inner bean '(inner bean)#6221ac7d' of type [com.oracle.cegbu.aconex.filter.RequestHeaderFilter] while setting bean property 'filter'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name '(inner bean)#6221ac7d' defined in file [/Users/sachinhr/Aconex/ora-acx-spring-bff/build/classes/java/main/com/oracle/cegbu/aconex/filter/RequestHeaderFilter.class]: Unsatisfied dependency expressed through constructor parameter 4; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'babylonSessionService' defined in file [/Users/sachinhr/Aconex/ora-acx-spring-bff/build/classes/java/main/com/oracle/cegbu/aconex/service/auth/BabylonSessionService.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'bffRestTemplate' defined in file [/Users/sachinhr/Aconex/ora-acx-spring-bff/build/classes/java/main/com/oracle/cegbu/aconex/common/delegate/BffRestTemplate.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'baseUrlProvider': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Circular placeholder reference 'instance' in property definitions
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:389) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:134) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1707) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.15.jar:5.3.15]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:212) ~[spring-boot-2.5.9.jar:2.5.9]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:203) ~[spring-boot-2.5.9.jar:2.5.9]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:97) ~[spring-boot-2.5.9.jar:2.5.9]
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:86) ~[spring-boot-2.5.9.jar:2.5.9]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:260) ~[spring-boot-2.5.9.jar:2.5.9]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:234) ~[spring-boot-2.5.9.jar:2.5.9]
at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) ~[spring-boot-2.5.9.jar:2.5.9]
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5219) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_312]
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[na:1.8.0_312]
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:835) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1396) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1386) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_312]
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[na:1.8.0_312]
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:919) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:263) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:432) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:927) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:486) ~[tomcat-embed-core-9.0.58.jar:9.0.58]
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123) ~[spring-boot-2.5.9.jar:2.5.9]
... 63 common frames omitted 

解决方案

从错误日志根因java.lang.IllegalArgumentException: Circular placeholder reference 'instance' in property definitions可知,问题出在配置文件的属性占位符循环引用,并非Filter本身实例化问题。

步骤1:修复配置文件循环占位符

检查项目配置文件(如application.properties/application.yml),找到包含instance的配置项,修正循环引用。例如:

# 错误示例:循环引用
instance=${instance}
# 修正:直接赋值或通过外部配置传入
instance=test-instance

步骤2:简化测试类Filter加载逻辑

集成测试中使用TestRestTemplate会启动真实Servlet容器,@ServletComponentScan已自动注册Filter,无需手动在MockMvc中添加。同时需给请求添加Authorization头避免Filter拦截报错:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ControllerTest {

    @LocalServerPort
    private int port;

    TestRestTemplate restTemplate = new TestRestTemplate();

    HttpHeaders headers = new HttpHeaders();

    @Test
    public void singleFieldProjectFields() throws Exception {
        // 添加Authorization头,绕过Filter的token校验
        headers.setBearerAuth("mock-valid-token");

        ObjectMapper mapper = new ObjectMapper();
        MailSearchModel mailSearchModel = mapper.readValue(new File("./src/test/resources/SingleValueSchema.json"),
                MailSearchModel.class);
        LinkedMultiValueMap<String, String> requestParams = new LinkedMultiValueMap<>();
        requestParams.add("sort_direction", "DESC");
        requestParams.add("page_number", "1");
        requestParams.add("sort", "sentdate");
        requestParams.add("page_size", "100");

        // 构造带参数的请求URL
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(createURLWithPort("/rest/projects/{projectId}/mails/filter/{type}"))
                .queryParams(requestParams);

        HttpEntity<MailSearchModel> entity = new HttpEntity<>(mailSearchModel, headers);
        ResponseEntity<String> response = restTemplate.exchange(
                builder.buildAndExpand("1879053221", "inbox").toUriString(), 
                HttpMethod.POST, 
                entity,
                String.class);

        String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);
        assertTrue(actual.contains("/mails"));
    }

    private String createURLWithPort(String uri) {
        return "http://localhost:" + port + uri;
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 13:35:23