Spring.NETでカスタム属性を使ったインジェクションを実現する

Spring.NETは非常に便利だが、プログラムがある程度の規模になってくると設定ファイルがかなり膨らんでくる。カスタムパーサーなどを使って記述量を減らしたりはできるが、たかがしれている。コンテナに登録するオブジェクトが少量でも、いちいち設定ファイルにしこしこと記述しなければならないため、とてもお手軽には使えるものではない。(まぁもともとエンタープライズ分野のものだが。。。)

じゃあお気軽に使えるように改造すればいいんじゃね?ということで、Spring.NETをお気楽に使えるように拡張する事にした。
やりたいことは、

  • カスタム属性でobjectを宣言できる
  • カスタム属性でobjectをプロパティにインジェクションできる
  • 設定ファイルは極力使わない

実装

まずはマークしたクラスをDIコンテナのオブジェクトとして登録するカスタム属性

using System;
using System.Diagnostics;

using Spring.Objects.Factory.Config;

namespace Spring.Extensions.Attributes {
    [Serializable]
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class ObjectAttribute : Attribute {
        private string name;

        /// <summary>遅延初期化するかどうか</summary>
        public bool IsLazyInit = true;
        /// <summary>Singletonオブジェクトかどうか</summary>
        public bool IsSingleton = true;
        /// <summary>AutoWiringモード</summary>
        public AutoWiringMode AutowireMode = AutoWiringMode.No;

        /// <summary>初期化メソッド名</summary>
        public string InitMethodName;
        /// <summary>抽象オブジェクトかどうか</summary>
        public bool IsAbstract = false;

        /// <summary>ファクトリオブジェクト名</summary>
        public string FactoryObjectName;
        /// <summary>ファクトリメソッド名</summary>
        public string FactoryMethodName;
        /// <summary>オブジェクト破棄メソッド名</summary>
        public string DestroyMethodName;

        /// <summary>
        /// オブジェクト名を取得します。
        /// </summary>
        public string Name {
            get {
                return name;
            }
        }

        public ObjectAttribute(string name) {
            this.name = name;
        }
    }
}

次はマークしたプロパティに値又はDIコンテナに登録されたオブジェクトをインジェクションするカスタム属性

using System;
using System.Diagnostics;

namespace Spring.Extensions.Attributes {
    [Serializable]
    [AttributeUsage(AttributeTargets.Property)]
    public class InjectionAttribute : Attribute {
        /// <summary>
        /// オブジェクト参照を表す接頭語
        /// </summary>
        private const string PREFIX_OBJREF = "ref:";
        
        private object value;
        private bool isObjectReference;

        /// <summary>
        /// インジェクトする値を取得します。
        /// </summary>
        public object Value {
            get {
                return value;
            }
        }
        /// <summary>
        /// この値がオブジェクト参照名かどうかを取得します。
        /// </summary>
        public bool IsObjectReference {
            get {
                return isObjectReference;
            }
        }

        public InjectionAttribute(object value) {
            string valueString = value.ToString();

            if(valueString.StartsWith(PREFIX_OBJREF)) {
                this.isObjectReference = true;
                this.value = valueString.Substring(PREFIX_OBJREF.Length);
            } else {
                this.value = value;
            }
        }
    }
}

引数で渡した値がただの値かオブジェクト名かを判断するのに、オブジェクト名では接頭語として「ref:」を付けるというルールにした。(なんとなくこれがイカスと思った)

で、これらの属性でマークされたクラスをどうやってSpringのコンテナに登録するか?それにはカスタムパーサーを利用する。カスタムパーサーを使うと独自の設定ファイルを読み込んで、Springのオブジェクト定義に変換することができる。
今回の場合は独自の設定ファイルに属性でマークされた型があるアセンブリアセンブリ名を記述しておき、そこから型を読み取ってSpringのオブジェクト定義に変換するというカスタムパーサーを作ればいい。

カスタムパーサー

カスタムパーサーを作るには、まず独自の設定ファイルのXMLスキーマを作る。

このスキーマに従ったXMLファイルは↓こんな感じ

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://spring.extensions.xsd">

    <assemblies>
        <assembly>アセンブリ名</assembly>
    </assemblies>
    
</objects>

このスキーマファイルを「spring-extensions.xsd」というファイル名にして、アセンブリに埋め込んでおく。

カスタムパーサーは「Spring.Objects.Factory.Xml.DefaultXmlObjectDefinitionParser」クラスを継承する。

using System;
using System.Xml;
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics;

using Spring.Objects;
using Spring.Objects.Factory.Xml;
using Spring.Objects.Factory.Config;

using Spring.Extensions.Attributes;

namespace Spring.Extensions {
    public class AnnotationObjectParser : DefaultXmlObjectDefinitionParser {
        public AnnotationObjectParser() {
        }
        
        public override int ParseRootElement(XmlElement root, XmlResourceReader parser) {
            XmlElement assembliesNode = (XmlElement)base.SelectSingleNode(root, "assemblies");

            List<string> loadAssemblies = new List<string>();
            // オブジェクト定義を読み込むアセンブリ
            foreach(XmlNode node in base.SelectNodes(assembliesNode, "assembly")) loadAssemblies.Add(node.InnerText);

            loadAssemblies.ForEach(delegate(string name) {
                Assembly asm = Assembly.Load(name);

                foreach(Type tp in asm.GetTypes()) {
                    object[] attrs = tp.GetCustomAttributes(typeof(ObjectAttribute), false);

                    if(attrs.Length > 0) {
                        RegisterObjectDefinition(parser, tp, (ObjectAttribute)attrs[0]);
                    }
                }
            });
            return base.ParseRootElement(root, parser);
        }

        /// <summary>
        /// 指定した型をSpringObjetとして現在のContextに登録します。
        /// </summary>
        /// <param name="parser"></param>
        /// <param name="tp"></param>
        /// <param name="objAttr">ObjectAttribute属性</param>
        private static void RegisterObjectDefinition(XmlResourceReader parser, Type tp, ObjectAttribute objAttr) {
            IConfigurableObjectDefinition objDef = parser.ObjectDefinitionFactory.CreateObjectDefinition(
                tp.FullName, null, null, null, parser.ObjectReader.Domain
            );
            objDef.IsLazyInit = objAttr.IsLazyInit;
            objDef.IsSingleton = objAttr.IsSingleton;
            objDef.AutowireMode = objAttr.AutowireMode;
            objDef.InitMethodName = objAttr.InitMethodName;
            objDef.IsAbstract = objAttr.IsAbstract;
            objDef.FactoryObjectName = objAttr.FactoryObjectName;
            objDef.FactoryMethodName = objAttr.FactoryMethodName;
            objDef.DestroyMethodName = objAttr.DestroyMethodName;

            // プロパティを解決
            foreach(PropertyValue pv in GetInjectionProperties(tp)) objDef.PropertyValues.Add(pv);
            // オブジェクトを登録
            parser.ObjectReader.Registry.RegisterObjectDefinition(objAttr.Name, objDef);
        }
        /// <summary>
        /// 指定した型からInjectionAttributeでマークされたプロパティの設定を取得します。
        /// </summary>
        /// <param name="tp"></param>
        /// <returns>プロパティ設定</returns>
        private static IEnumerable<PropertyValue> GetInjectionProperties(Type tp) {
            foreach(PropertyInfo propInfo in tp.GetProperties()) {
                if(!propInfo.CanWrite) continue;

                object[] attrs = propInfo.GetCustomAttributes(typeof(InjectionAttribute), false);

                if(attrs.Length > 0) {
                    InjectionAttribute injectAttr = (InjectionAttribute)attrs[0];

                    yield return new PropertyValue(propInfo.Name,
                        injectAttr.IsObjectReference ?
                            new RuntimeObjectReference(injectAttr.Value.ToString()) : injectAttr.Value
                    );
                }
            }
        }
    }
}

「ParseRootElement」メソッドをOverrideする。

  1. まずは定義ファイルからアセンブリ名の一覧を読み込んできて、リストにまとめる。
  2. そのアセンブリを読み込み、「ObjectAttribute」属性でマークされた型を探す。
  3. その型から「parser.ObjectDefinitionFactory.CreateObjectDefinition」メソッドでオブジェクト定義を作る。
  4. 「InjectionProperty」属性でマークされているプロパティを探す。
  5. オブジェクト参照の場合は「RuntimeObjectReference」クラスを、そうでなければそのままの値で「PropertyValue」クラスをインスタンス化する。
  6. それをオブジェクト定義に追加する。
  7. オブジェクト定義をコンテキストに登録する。

これで完了。

使い方

Hoge」クラス。
hoge」という名前でコンテナに登録し、「Name」プロパティに「Hello」という文字列を設定する。

[Object("hoge")]
public class Hoge {
    private string name;

    [Injection("Hello")]
    public string Name {
        private get { return name; }
        set { name = value; }
    }
    
    public Hoge() {
    }
}

「HogeManager」クラス。
「hogeManager」という名前で登録し、「Hoge」プロパティに先程の「hoge」オブジェクトを設定する。

[Object("hogeManager")]
public class HogeManager {
    private Hoge hoge;
    
    [Injection("ref:hoge")]
    public Hoge Hoge {
        private get { return hoge; }
        set { hoge = value; }
    }
    
    public HogeManager() {
    }
}

独自の設定ファイル(App.spring.config)

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://spring.extensions.xsd">

    <assemblies>
        <assembly>Spring.Extensions.Test</assembly>
    </assemblies>
    
</objects>

アプリケーション構成ファイル

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="parsers" type="Spring.Context.Support.ConfigParsersSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="file://App.spring.config" />
        </context>
        <parsers>
            <parser namespace="http://spring.extensions.xsd"
                type="Spring.Extensions.AnnotationObjectParser, Spring.Extensions"
                schemaLocation="assembly://Spring.Extensions/Spring/spring-extensions.xsd"
            />
        </parsers>
    </spring>

</configuration>

「parsers」という構成セクションを追加して、「spring/parsers」にカスタムパーサーを追加する。

エントリーポイント

public static class Program {
    static void Main() {
        IApplicationContext context = (IApplicationContext)ConfigurationManager.GetSection("spring/context");

        HogeManager hogeManager = (HogeManager)context.GetObject("hogeManager");
    }
}

いつもどおりのやり方でオブジェクトを取得できる。

足りない機能は山ほどあるけど、さくっと使う分にはこれで十分だと思う。

ちなみに通常の設定ファイルを使うと以下のようになる。

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">
    
    <object name="hoge" type="Spring.Extensions.Hoge, Spring.Extensions.Test">
    </object>
    
    <object name="hogeManager" type="Spring.Extensions.HogeManager, Spring.Extensions.Test">
        <property name="Hoge" ref="hoge" />
    </object>
    
</objects>