Skip to content

Conversation

yokotaso
Copy link
Contributor

@yokotaso yokotaso commented Nov 13, 2019

As of Spring Framework 5.2, the way of define order value is changed.
9239ab1#diff-e6efe5c07f3b7003d9229a7140948cebL120
9239ab1#diff-e6efe5c07f3b7003d9229a7140948cebL243-L249

But This change breaks backward compatibility,
9239ab1#diff-e6efe5c07f3b7003d9229a7140948cebR136-R147

In this method, order will be decided by Spring component instance primary,
But before Spring Framework 5.2.0, order will be decided from bean type if beanType is not null.
In Spring Framework 5.2,

  1. org.springframework.web.method.ControllerAdviceBean#getOrder is called in spring initialize phase
    (Thread of initialize phase is request or session scope)
  2. thread of initialize get order of component with trying initiate component via BeanFactory
  3. BeanFactory throws Exception because thread of (2) is different from @Scope annotated component(ex. scope is like request, session...etc)

If I integrate my applicaiton with Spring Boot 2.2.1, Application won't be failed to start.

2019-11-13 07:43:05,313 ERROR [] [main] [N/A] SpringApplication - Application run failed
org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Jetty web server
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:156)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:544)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
	at <My Application Code>
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Jetty web server
	at org.springframework.boot.web.embedded.jetty.JettyWebServer.initialize(JettyWebServer.java:114)
	at org.springframework.boot.web.embedded.jetty.JettyWebServer.<init>(JettyWebServer.java:86)
	at org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory.getJettyWebServer(JettyServletWebServerFactory.java:401)
	at <My Application Code>(<My JettyServletWebServerFactory.java>:59)
	at org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory.getWebServer(JettyServletWebServerFactory.java:155)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:180)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:153)
	... 7 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '<My Application Class Component>': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1803)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:595)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:879)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
	at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:401)
	at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:292)
	at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:103)
	at <My Customized ContextLoaderListener>ContextLoaderListener.contextInitialized(<My Customized ContextLoaderListener>.java:23)
	at org.eclipse.jetty.server.handler.ContextHandler.callContextInitialized(ContextHandler.java:921)
	at org.eclipse.jetty.servlet.ServletContextHandler.callContextInitialized(ServletContextHandler.java:554)
	at org.eclipse.jetty.server.handler.ContextHandler.startContext(ContextHandler.java:888)
	at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:357)
	at org.eclipse.jetty.webapp.WebAppContext.startWebapp(WebAppContext.java:1443)
	at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1407)
	at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:821)
	at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:276)
	at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:524)
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:72)
	at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:169)
	at org.eclipse.jetty.server.Server.start(Server.java:407)
	at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:110)
	at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:106)
	at org.eclipse.jetty.server.Server.doStart(Server.java:371)
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:72)
	at org.springframework.boot.web.embedded.jetty.JettyWebServer.initialize(JettyWebServer.java:108)
	... 13 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name '<My Application Class Component>': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:368)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1108)
	at org.springframework.web.method.ControllerAdviceBean.resolveBean(ControllerAdviceBean.java:173)
	at org.springframework.web.method.ControllerAdviceBean.getOrder(ControllerAdviceBean.java:139)
	at org.springframework.core.OrderComparator.findOrder(OrderComparator.java:146)
	at org.springframework.core.OrderComparator.getOrder(OrderComparator.java:129)
	at org.springframework.core.OrderComparator.getOrder(OrderComparator.java:117)
	at org.springframework.core.OrderComparator.doCompare(OrderComparator.java:87)
	at org.springframework.core.OrderComparator.compare(OrderComparator.java:73)
	at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355)
	at java.util.TimSort.sort(TimSort.java:220)
	at java.util.Arrays.sort(Arrays.java:1512)
	at java.util.ArrayList.sort(ArrayList.java:1462)
	at org.springframework.core.OrderComparator.sort(OrderComparator.java:175)
	at org.springframework.web.method.ControllerAdviceBean.findAnnotatedBeans(ControllerAdviceBean.java:238)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.initControllerAdviceCache(RequestMappingHandlerAdapter.java:580)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.afterPropertiesSet(RequestMappingHandlerAdapter.java:559)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1862)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1799)
	... 43 common frames omitted

So It is good, try to decide order by beanType primarily, I think.

As of Spring Framework 5.2, the way of define order value is changed.
9239ab1#diff-e6efe5c07f3b7003d9229a7140948cebL120
9239ab1#diff-e6efe5c07f3b7003d9229a7140948cebL243-L249

But This change breaking backward compatibility,
9239ab1#diff-e6efe5c07f3b7003d9229a7140948cebR136-R147

In this method, order will be decided by Spring component instance primary,
But before Spring Framework 5.2.0, order will be decided from bean type if beanType is not null.
In Spring Framework 5.2,

1. org.springframework.web.method.ControllerAdviceBean#getOrder is called in spring initialize phase
   (Thread of initialize phase is request or session scope)
2. thread of initialize get order of component with trying initiate component via BeanFactory
3. BeanFactory throws Exception because thread of (2) is different from @scope annotated component(ex. scope is like request, session...etc)
@yokotaso yokotaso changed the title Fix breaking change in #getOrder logic Fix breaking change in ControllerAdviceBean#getOrder logic Nov 13, 2019
@pivotal-issuemaster
Copy link

@yokotaso Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 13, 2019
@rstoyanchev rstoyanchev added in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Nov 13, 2019
@rstoyanchev rstoyanchev added this to the 5.2.2 milestone Nov 13, 2019
@rstoyanchev
Copy link
Contributor

@sbrannen I've scheduled this as it does look like a regression. As for the solution, maybe we should avoid resolving the bean unless we know it is Ordered?

@rstoyanchev rstoyanchev changed the title Fix breaking change in ControllerAdviceBean#getOrder logic ControllerAdviceBean#getOrder causes BeanCreationException due to early initialization Nov 13, 2019
@sbrannen
Copy link
Member

@sbrannen I've scheduled this as it does look like a regression.

Thanks

As for the solution, maybe we should avoid resolving the bean unless we know it is Ordered?

Perhaps. I'll have to investigate.

@sbrannen sbrannen self-assigned this Nov 13, 2019
@sbrannen
Copy link
Member

At a quick glance, it would appear that our assumption in #23163 (comment) was incorrect.

@sbrannen sbrannen changed the title ControllerAdviceBean#getOrder causes BeanCreationException due to early initialization Regression: ControllerAdviceBean#getOrder causes BeanCreationException due to early initialization Nov 13, 2019
@sbrannen
Copy link
Member

I am able to reproduce this exception with the following simplified test case.

class ControllerAdviceBeanIntegrationTests {

	@Test
	void loadContextWithRequestScopedControllerAdviceBean() {
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
		context.setServletContext(new MockServletContext());
		context.register(Config.class);
		context.refresh();

		context.close();
	}


	@ControllerAdvice
	static class RequestScopedControllerAdvice implements Ordered {

		@Override
		public int getOrder() {
			return 45;
		}

	}

	@Configuration
	@EnableWebMvc
	static class Config {

		@Bean
		@RequestScope
		RequestScopedControllerAdvice requestScopedControllerAdvice() {
			return new RequestScopedControllerAdvice();
		}
	}

}

@sbrannen sbrannen changed the title Regression: ControllerAdviceBean#getOrder causes BeanCreationException due to early initialization Regression: ControllerAdviceBean#getOrder() causes BeanCreationException for request scoped advice beans Nov 17, 2019
@pivotal-issuemaster
Copy link

@yokotaso Thank you for signing the Contributor License Agreement!

@sbrannen
Copy link
Member

So It is good, try to decide order by beanType primarily, I think.

That's not a valid option, since Ordered semantics should always override @Order semantics.

In light of that, I am going to introduce a check that avoids eager @ControllerAdvice bean resolution for scoped proxies, and I'll update the Javadoc to point out that Ordered will not be supported if a @ControllerAdvice bean is configured as a scoped proxy (e.g., a request or session scoped bean).

@sbrannen
Copy link
Member

@yokotaso, thanks for the PR and for bringing this regression to our attention!

As I mentioned above, I took a slightly different approach to the fix which you can see in 3a39b7f.

@yokotaso yokotaso deleted the breaking-change-order branch November 19, 2019 01:53
@yokotaso
Copy link
Contributor Author

@sbrannen I realized your thought. Thank you for fix problem 👍

sbrannen added a commit that referenced this pull request Nov 19, 2019
sbrannen added a commit that referenced this pull request Nov 19, 2019
The test introduced in this commit is intended to serve as a regression
test for the status quo.

See gh-23985
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: regression A bug that is also a regression
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants