読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり、所属会社の見解ではありません。 GitHub: takahashikzn

component-scanでプレースホルダーを使えるようにする

Spring

Springの設定ファイルを共通化する際、プレースホルダーを使うことがよくあります。


例えば

<context:property-placeholder location="classpath:jdbc.properties"/>

<bean id="dataSource" 
  class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">

  <property name="targetDataSource">
    <bean class="org.apache.tomcat.jdbc.pool.DataSource"
      p:driverClassName="${jdbc.driver}"
      p:url="${jdbc.url}"
      p:username="${jdbc.username}"
      p:password="${jdbc.password}"
      />
  </property>
</bean>

というXMLがある時、jdbc.propertiesだけ書けば良いわけです。

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/test
jdbc.username=test
jdbc.password=test

こういうの、きっとどこかで見たことがあるはず。

component-scanでも...

で、component-scanでもプレースホルダーが使えると思うじゃないですか、普通に。

だから、

<context:component-scan
  base-package="${service.package}"
  use-default-filters="false">

  <context:include-filter type="annotation"
    expression="org.springframework.stereotype.Repository" />

</context:component-scan>

のように書いてアプリを起動したわけですが、

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'service.package' in string value "${service.package}"
	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(
PropertyPlaceholderHelper.java:173)
	at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(
PropertyPlaceholderHelper.java:125)
	at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(
AbstractPropertyResolver.java:180)
	at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(
AbstractPropertyResolver.java:145)
	at org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders(
AbstractEnvironment.java:472)
	...

というふうに動かないわけで、えーマジっすか、と思うわけです。


ググったところ、

こんなものを発見。
https://jira.springsource.org/browse/SPR-4351


要旨は以下の通り。かなり手抜きな訳なのでご注意を。

<context:component-scan base-package="${base.package}"/>



The ${base.package} never get's resolved when ComponentScanBeanDefinitionParser pulls it from the attribute.

I realize this is probably a much larger issue potentially involving certain attributes of many NamespaceHandlers.

However, I thought I'd create an issue for it since I didn't see any other mention of how it can be difficult identifying which attributes of a NamespaceHandler can accept property placeholders and which cannot.

Perhaps for the time being simply documenting in the XSD if the attribute can accept a property placeholder or not might help?



XMLで${base.package}のように属性を指定しても、ComponentScanBeanDefinitionParserはうまく解釈してくれない。
たぶん根本原因は面倒な問題なんだろうな、という気がしてるんだけど、探しても何も情報が出てこなかったのでチケットにしといた。
とりあえず、そもそもプレースホルダーを使えるかどうかくらいは、ドキュメントに書いといた方がいいんじゃね?

Juergen Hoeller added a comment - 17/Jan/08 6:06 AM



The general rule is that placeholders are only supported for bean definition metadata.

So for any XML element attributes that get translated into bean definition metadata such as property values, placeholders will take effect.

However, placeholders are not supported for configuration element settings which are relevant for deciding whether/how to build bean definitions in the first place.

This affects import locations and the names of scanned packages.

Admittedly, this isn't properly documented anywhere. I'll turn this into a documentation issue for the time being, as you suggested...



基本的に、プレースホルダーは『Bean定義用の属性(bean definition metadata)』のみで使えるのであって、『設定用の属性(configuration element)』では使えない。
ここで、設定と言っているのは『どのようにBean定義を構成するか』を指定している属性のことだ。例えば、importとかcomponent-scanなどが該当する。
確かにこのことはドキュメントに載ってないので、とりあえず書いとくべきだな…

Mike Youngstrom added a comment - 17/Jan/08 6:29 AM



I'd like to note that I think the general rule of property placeholders not working in XML element attributes is a poor general rule.

However, I understand that this is not small issue. Perhaps for Spring 3.0 something can be done to make a property placeholder type of mechanism more of a first class citizen with the BeanFactory?

I think the concept of a property placeholder has been proven useful and having no way to do a placeholder replacement of an XML element can lead to some confusion and frustration to users as NamespaceHandlers become more and more prevalent in Spring.


XMLの属性でプレースホルダーを使えないってのはお粗末だな。
これはそれなりに深刻な問題だと思うぞ。Spring3.0ではちゃんと使えるようにようになるんだろうな?
俺はプレースホルダーはかなり役立つ仕組みだと思ってる。

Juergen Hoeller added a comment - 01/Feb/08 9:13 AM



This doesn't actually have as much to do with NamespaceHandlers as you imply: Almost all NamespaceHandlers do a straight translation into bean definitions,
in which case placeholders can be used in virtually all attributes of the namespace element,
since they'll end up as bean definition metadata - which will get post-processed before it becomes active.

The problem is rather a lifecycle issue: Component scanning happens during the bean definition reading phase,
since it results in the creation of new bean definitions.

Then the ApplicationContext lifecycle kicks in, searching for post-processors among those beans and invoking them. For that reason,
a PropertyPlaceholderConfigurer will apply to those bean definitions just like to regular bean definitions,
but it won't apply to the component scan settings themselves.

So technically, this is not so much about properly placeholder parsing being a first-class feature of the core BeanFactory or not.

It's about the configurability through bean definitions and post-processors.

Custom placeholder configurer beans could override or post-process placeholders in custom ways; such post-processor beans could even be detected through component scanning!

It's a chicken-and-egg problem that is pretty hard to resolve without losing that customization power.

What we do for standard resource locations (all properties / attributes that take a Spring resource location, be it file system or classpath) is that we resolve "${...}" placeholders there against system properties.
I've applied this to component-scan base packages as well now, for consistency with resource locations.
Admittedly, system properties don't help much for typical scanning in web application environments... but it's better than nothing.



ちょっと違う。プレースホルダーはXMLの属性で問題なく使える。ただしBean定義用の設定だけだ…つまり、Beanがロード(≒インスタンス化)される段階での処理限定ということ。
問題はそこにはなくて、Beanのライフサイクルにある。コンポーネントのスキャンはBean定義の読み込み段階であって、それが完了して初めてBean定義が生成される。

ApplicationContextのライフサイクルにあるBeanのポストプロセッサではPropertyPlaceholderConfigurerは適用されるんだが、
コンポーネントのスキャンはそのライフサイクル外なので、プレースホルダーの置換が働かない。


この問題、プレースホルダーの解釈そのものは難しい話じゃなくて、設定の柔軟性の維持に関わるる話だ。
要するに、「鶏が先か卵が先か」問題であって、落とし所を見つけるのが難しい。


リソースのロケーションパス内のプレースホルダーについては、Java環境変数に限定されるけども解決できるようになっているんだが、
component-scanのパッケージ名指定でも同じことができるようにしたぞ。まあ、あまり役に立たんだろうがな。。。無いよりマシ程度。

ま、理屈はなんでもいいけどさ…


まあ色々議論はあったようですが、僕はとにかくcomponent-scanでプレースホルダーを使いたかったので、Springのクラスを拡張して無理やり動くようにしました。以下の通り。

/** PUBLIC DOMAIN */
package abc;


public class MyContextLoaderListener
    extends org.springframework.web.context.ContextLoaderListener {

    private final Properties props = new Properties();

    private void loadEnvProperties(final String[] locations) {

        for (final String loc : locations) {
            final String res = "/" + loc;

            try (InputStream src = this.getClass().getResourceAsStream(res)) {
                this.props.load(src);
            } catch (final IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private String replaceEnvProperty(final String text) {

        String value = text;

        for (final Object key : props.keySet()) {
            value = value.replaceAll("\\$\\{" + key + "\\}", props.getProperty((String) key));
        }

        return value;
    }

    @Override
    protected WebApplicationContext createWebApplicationContext(final ServletContext sc) {

        this.loadEnvProperties(this.getEnvPropLocation(sc));

        final ConfigurableWebApplicationContext context =
            (ConfigurableWebApplicationContext) super.createWebApplicationContext(sc);

        final StandardServletEnvironment environment = new StandardServletEnvironment() {
            @Override
            public String resolveRequiredPlaceholders(final String text) {
                return super.resolveRequiredPlaceholders(replaceEnvProperty(text));
            }
        };

        context.setEnvironment(environment);

        return context;
    }

    private String[] getEnvPropLocation(final ServletContext sc) {

        final String location = sc.getInitParameter("springContextEnvPropertyLocation");

        return location.trim().split("[\\s,]+");
    }
}

キモはStandardServletEnvironmentを拡張している箇所のみで、その他のコードはプレースホルダーのプロパティファイルを探してきてロードしているだけ。


そして、web.xmlでこのようにします。

<listener>
  <listener-class>abc.MyContextLoaderListener</listener-class>
</listener>

<context-param>
  <param-name>springContextEnvPropertyLocation</param-name>
  <param-value>aaa.properties, bbb.properties</param-value>
</context-param>