با Annotation Blog یک Ioc Container بسازید
Annotation ها از جاوا ۵ به بعد معرفی شده اند. Annotation ها metadata به پروژه شما اضافه میکنند که هنگام compile time و یا run time قابل استفاده می باشد. در این مقاله سعی میشود تا به صورت اتوماتیک و با استفاده از Annotation ها , component dependencies را انجام دهیم.
در IoC و یا DI,و component های پروژه هرگز در subcomponent ها ساخته نمیشوند بلکه نوع dependencies مورد نیازی برای component مشخص می گردد. میتوان اینطور بیان کرد که وظایف کامپوننت های سرویس ها حذف می گردد و بدین ترتیب دیگر نیازی نیست نگران تعریف آن باشیم. در این تکنولوژی وظایف به container component مرتبط میشود که ساختن و یا مدیریت کردن کامپوننت هارا بر عهده دارد. این سناریو در چندین container معروف مانند : PicoContainer و Spring پیاده شده است و فریم ورک هایی از این قبیل توانایی ایجاد تمام dependencies ها را برای component ها دارند. این توانایی یکی از ویژگی های کلیدی EJB3 Desing میباشد.
در property- or setter-based injection ,وcontainer که مدیریت dependencies ها را بر عهده دارد با بررسی تمام متد های setter و فیلد ها سعی در پیدا کردن dependencies هایی میباشد که با نام property و setter متناسب باشد. در نتیجه کار برای developer ها ساده تر میشود و فقط لازم است تا متد setter را به پروژه خود اضافه کنند مانند :
setAccoundingDataSource(DataSource ds)
سپس container فیلدی با نام AccountingDataSource از نوع DataSource را جستجو میکند. در اینجا automatic dependency resolution اتفاق می افتد و dependency بر اساس نام property تعریف میشود. کار container برای inject و تعیین dependency را میتوان به صورت اجمالی چنین در نظر بگیریم :
public static Object buildWithSetters( String name, Class<?> c, Map<String, Object> ctx) { try { Object component = c.newInstance(); Method[] methods = c.getMethods(); for( int i = 0; i<methods.length; i++) { Method m = methods[ i]; String mname = m.getName(); Class[] types = m.getParameterTypes(); if(mname.startsWith( "set") && types.length==1 && m.getReturnType()==Void.TYPE) { String dname = mname.substring( 3); m.invoke( component, new Object[] { ctx.get( dname.toLowerCase())}); } } ctx.put(name, component); return component; } catch( Exception ex) { S tring msg = "Initialization error"; throw new RuntimeException( msg, ex); } }
نام پارامتر همان نام component است که باید تعیین شود.
پارامتر های کلاس به عنوان نوع component میباشد.
ctx Map یک collection از component های تعریف شده میباشد.
component را به این صورت در نظر میگیرم :
public class Replicator { private DataSource input; private DataSource output; public void setInput(DataSource input) { this.input = input; } public void setOutput(DataSource output) { this.output = output; } public void replicate() { // ... } }
مشکلی که در اینجا ممکن است رخ دهد این است که اگر dependency توسط container که وظیفه مدیریت component را برعهده دارد , به درستی تعریف نشود component کار خود را بدرستی انجام نمیدهد زیرا instance ساخته شده از روی آن ممکن است تغییر کند و عملا inject های انجام شده به درستی وظیفه خود را انجام ندهد.
در constructor-based dependency injection,وcomponent شامل یک constructor میباشد که توسط آن تمام dependency های مورد نیاز تعریف میشوند. container تمام dependency های مورد نیاز را به هنگام تعریف component برای آن تعیین میکند و مشکلی که در property- or setter-based injection وجود داشت در اینجا بر طرف میشود. شیوه کار constructor-based dependency injection به این صورت میباشد که component موظف می شود تا تمام dependency های لازم را در هنگام ساخت به constructor ارجا دهد. همچنین این روش راه بهتری برای جلوگیری از اعمال تغییرات در dependency میباشد زیرا در این روش دیگر متد ها در معرض تغییرات قرار نمیگیرند.
نکته ای که در اینجا وجود دارد این است که Java Reflection Api اجازه بازیابی نام پارامتر هارا درون متد ها و constructor نمیدهد. در نتیجه فقط میتوان از روی نوع dependency ها, آنهارا جستجو کرد:
public static Object buildWithConstructor( String name, Class<?> c, Map<String,Object> ctx) { try { Constructor[] constructors = c.getDeclaredConstructors(); assert constructors.length!=1 : "Component must have single constructor"; Constructor cc = constructors[ 0]; Class[] types = cc.getParameterTypes(); Object[] params = new Object[ types.length]; for( int i = 0; i < names.length; i++) { params[ i] = context.get( types[ i]); } Object component = cc.newInstance( params); ; ctx.put(name, component); return component; } catch( Exception ex) { String msg = "Initialization error"; throw new RuntimeException( msg, ex); } }
و component را میتوان به این صورت نوشت :
public class Replicator { private final DataSource input; private final DataSource output; public Replicator( DataSource input, DataSource output) { this.input = input; this.output = output; } public void replicate() { // ... } }
همانطور که میبینید کد بسیار مختصر تر شده است اما یک مشکل در اینجا وجود دارد. نمیتوان dependency را به صورت اتومات تعریف کرد زیرا دو dependency با نوع یکسان داریم. این مشکل را میتوان به عنوان محدودیت دراین روش دانست و میتوان نتیجه گرفت که در این روش component تنها میتواند یک نوع dependency داشته باشد.
برای اینکه اطلاعات پارامتر های constructor در هنگام اجرا قابل دسترسی باشد از annotation ها برای مشخص کردن اطلاعات پارامتر ها و متد ها استفاده میکنیم :
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.CONSTRUCTOR}) public @interface Ref { String[] value() ; }
و برای استفاده از این annotation درون constructor :
public class Replicator( @Ref("input") DataSource input, @Ref("output") DataSource output) { this.input = input; this.output = output; }
به این صورت عمل میکنیم. حالا میتوانیم بر اساس نام dependency ها constructor base injection را انجام دهیم :
public static Object buildWithConstructor( String name, Class<?> c, Map<String,Object> ctx) { try { Constructor[] constructors = c.getDeclaredConstructors(); assert constructors.length!=1 : "Component must have single constructor"; Constructor<?> cc = constructors[ 0]; Class[] types = cc.getParameterTypes(); Annotation[][] anns = cc.getParameterAnnotations(); String[] names = new String[ types.length]; for( int i = 0; i<anns.length; i++) { Annotation[] ann = anns[ i]; for( int j = 0; j<ann.length; j++) { if( ann[ j] instanceof Ref) { String[] v = (( Ref) ann[ j]).value(); names[ i] = v[ 0]; } } } Object[] params = new Object[ types.length]; for( int i = 0; i<types.length; i++) { params[ i] = ctx.get( names[ i]); } Object component = cc.newInstance( params); ; ctx.put(name, component); return component; } catch( Exception ex) { String msg = "Initialization error"; throw new RuntimeException( msg, ex); } }
متد getParameterAnnotations یک آرایه ای از آرایه ها را بر میگرداند. اندازه اولین آرایه برابر با تعداد پارامتر های درون constructor میباشد. باقی آرایه ها متناظر با پارامتر constructor می باشد و در صورتی که اندازه آرایه برابر با صفر باشد به این معناست که برای آن پارامتر, annotation تعریف نشده است. این روش برای باقی پارامتر ها نیز تکرار میشود و از طریق متد ref مقدار را میگیرد.
همچنین میتوان یک generic service factory را پیاده سازی کرد. برای اینکار نیاز است تا یک map برای نوع dependency نیز مشخص گردد. مانند کد زیر
public interface ReplicatorFactory { Replicator getForwardReplicator(); Replicator getBackwardReplicator(); }
در پیاده سازی ReplicatorFactory باید component را با نام dependency متناسب با آن نگاشت کرد. این روش هم برای constructor base و هم برای property base قابل استفاده می باشد.
public class ReplicatorFactoryImpl implements ReplicatorFactory { private final Map<String, Object> ctx; public ReplicatorFactoryImpl(Map<String,Object> ctx) { this.ctx = ctx; } public Replicator getForwardReplicator() { Map<String,Object> childCtx = new HashMap<String, Object>(); childCtx.put("input",ctx.get("source1")); childCtx.put("output",ctx.get("source2")); return (Replicator) Builder.buildWithConstructor( "forwardReplicator", Replicator.class, childCtx); }
کد بالا برای انجام چنین کاری ممکن است پیچیده باشد ولی میتوانیم با annotation ها این موضوع را ساده تر انجام دهیم :
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD} ) public @interface Mapping { String param(); String ref(); }
و همچنین برای نگاشت چندتایی :
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.CONSTRUCTOR}) public @interface Ref { String[] value() default {} ; Mapping[] mappings() default {} ; }
و اینچنین با annotation ها میتوان ReplicationFactory به این صورت نوشت :
public interface ReplicatorFactory { @Ref( mappings={ @Mapping( param="input", ref="source1"), @Mapping( param="output", ref="source2") }) Replicator getForwardReplicator(); @Ref( mappings={ @Mapping( param="input", ref="source1"), @Mapping( param="output", ref="source2") }) Replicator getBackwardReplicator(); }
و میتوان آنرا به صورت dynamic ایجاد کرد :
public static Object buildFactory( Class c, Map<String,Object> context) { return Proxy.newInstance( c.getClassLoader(), new Class[] { c}, new AutoWireInvocationHandler(c, context)); }
تمام اعمال مرتبط با dependency injection در کلاس AutoWireInvocationHandler صورت میگیرد به طوری که تمام متد ها تعریف میشوند و یک instance از روی نوع component ساخته شده و برمیگردد. پیاده سازی کلاس InvocationHandler میتواند به این صورت باشد :
public final class AutoWireInvocationHandler implements InvocationHandler { private Map<String, Object> services = new HashMap<String, Object>(); public AutoWireInvocationHandler( Class c, Map<String,Object> ctx) throws Exception { Method[] methods = c.getDeclaredMethods(); for( int i = 0; i<methods.length; i++) { Method m = methods[ i]; Ref ref = m.getAnnotation(Ref.class); if( ref!=null) { services.put( m.getName(), createInstance( m.getReturnType(), ref.mappings(), ctx)); } } } private Object createInstance(Class<?> type, Mapping[] mappings, Map<String,Object> ctx) { Map<String,Object> childCtx = new HashMap<String, Object>(); for( int i = 0; i < mappings.length; i++) { Mapping m = mappings[ i]; childCtx.put(m.param(), ctx.get(m.ref())); } return Builder.buildWithConstructor(type, childCtx); } public Object invoke( Object proxy, Method m, Object[] args) { return services.get( m.getName()); } }
متد createInstance مانند getForwardReplicator عمل نگاشت dependency هارا را انجام میدهد. در کد بالا component ساخته شده در هر متد درون map نگهداری میشود و factory در هر فراخوانی component مورد نظر آنرا برمیگردانند.
دیدگاهتان را بنویسید
برای نوشتن دیدگاه باید وارد بشوید.