Jim Garrison
Jim Garrison

Reputation: 86774

Scope of <context:component-scan.../> and <context:property-placeholder.../> in hierarchical contexts

I have read:

Multiple component-scan

What is the difference between ApplicationContext and WebApplicationContext in Spring MVC?

@RequestMapping annotation not working if <context:component-scan /> is in application context instead of dispatcher context (more on this one later)

and several others but none of these answers the question:

Why is the scope of <context:component-scan.../> limited when it is present in the ROOT context of a Spring MVC application?

My understanding is that it causes scanning of all classes in the specified packages and instantiates any beans stereotyped with @Component or any of its sub-stereotypes (@Repository, @Service and @Controller).

Given:

applicationContext.xml (root context)

<beans...>
    ...
    <context:component-scan base-package="com.myproject"/>
    <context:property-placeholder 
               ignore-resource-not-found="true" 
               location="classpath:default.properties, file:///etc/gallery/gallery.properties"/>
</beans>

main-servlet.xml (servlet context)

<beans ...>
    ...
    <mvc:annotation-driven/>
    <mvc:resources mapping="/image/**"   location="file:/${gallery.location}" />
    <mvc:resources mapping="/css/**"     location="/css/"/>
    <mvc:resources mapping="/js/**"      location="/js/"/>
    <mvc:resources mapping="/images/**"  location="/images/"/>
    ...
</beans>

com/myproject/web/MainController.java

package com.myproject.web;
@Controller
public class MainController 
{
    ...

    @RequestMapping("/gallery/**")
    public String gallery(ModelMap modelMap, HttpServletRequest req, HttpServletResponse resp) throws IOException
    {
        ...
    }
}

The Spring docs state that any beans instantiated in the root context are shared and available to the individual servlet application contexts. Thus the two <context:...> declarations in the root context should result in beans that are visible in the servlet context. But this does not appear to be the case. I am required to repeat BOTH <context:component-scan.../> and <context:property-placeholder.../> in the servlet context.

Omitting the <context:component-scan.../> in the servlet context results in

Sep 15, 2015 10:08:16 AM org.springframework.web.servlet.PageNotFound noHandlerFound
WARNING: No mapping found for HTTP request with URI [/gallery/habitat/20150813] in DispatcherServlet with name 'main'
Sep 15, 2015 10:08:16 AM org.springframework.web.servlet.PageNotFound noHandlerFound
WARNING: No mapping found for HTTP request with URI [/error] in DispatcherServlet with name 'main'

indicating that the @Controller was not resolved.

Omitting the <context:property-placeholder.../> results in a @Value annotation using a property reference not being processed, which in my case results in some broken links.

Since both these <context:.../> directives result in bean instantiations, I'm confused as to why the beans are not visible in the child context, in direct contradiction to the documentation. Also, doesn't having two component-scan statements cause the controller bean to be instantiated twice?

With regards to @RequestMapping annotation not working if <context:component-scan /> is in application context instead of dispatcher context, I do have <mvc:annotation-driven /> in my app context, and the answers here do not explain WHY two component-scan statements are necessary.

I'm really uncomfortable using "magic" unless I completely understand how it works and can predict how it will behave when I tweak something. So the "solution" of "just add it in both places and move on" is unacceptable.

Upvotes: 4

Views: 2109

Answers (1)

M. Deinum
M. Deinum

Reputation: 124760

<context:property-placeholder />

The <context:property-placeholder /> registers a PropertySourcesPlaceholderConfigurer which is a [BeanFactoryPostProcessor]. The key here is BeanFactory, it operates on a BeanFactory (the ApplicationContext is such a thing). To be precise it operates on the BeanFactory it is defined in. Not on parent or child contexts. Hence you need to register it in both contexts.

There is one drawback of having 2 the same <context:property-placeholder /> both will load the same resource and you endup loading the same properties file twice. To eliminate this combine the <context:property-placeholder /> with a <util:properties /> element. The latter loads properties file(s) and exposes them as bean in the context, you can wire this bean to the <context:property-placeholder /> using the properties-ref attribute. You only load the properties in the root context and simple refer to them in child contexts.

Root Context

<util:properties id="appProperties" location="classpath:default.properties, file:///etc/gallery/gallery.properties" ignore-resource-not-found="true" />
<context:property-placeholder properties-ref="appProperties" />

Child Context

<context:property-placeholder properties-ref="appProperties" />

<mvc:annotation-driven />

Regarding the <mvc:annotation-driven /> that registers, amongst others, a RequestMappingHandlerMapper, this bean is responsible for detecting methods annotated with @RequestMapping in @Controller annotated classes. By default it does this in the ApplicationContext it is defined in NOT in parent contexts. You could set the detectHandlerMethodsInAncestorContexts property to true to make this happen.

As a rule of thumb you could say that your root context should contain everything application global like services, repositories, infrastructure beans like datasources etc. The child contexts (loaded by the DispatcherServlet should only contain web related materials like @Controllers.).

Just copy pasting the <context:component-scan />, literally duplicating it, is a bad (very bad) idea. As this will lead to instantiate all your beans twice and might lead to issues with transactions, memory etc. Because things like <tx:annotation-driven /> work with AOP and AOP is also applied using a BeanFactoryPostProcessor and BeanPostProcessor just like the property support.

Root Context

<context:component-scan base-package="com.myproject">
    <context:exclude-filters type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

Child Context

<context:component-scan base-package="com.myproject" use-default-filters="false">
    <context:include-filters type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

Upvotes: 4

Related Questions