دانستنی‌ها

Bijection فراتر از dependency injection (جشنواره عید تا عید)

(این مطلب توسط آقای امید پورهادی برای جشنواره عید تا عید جاواکاپ ارسال شده است و محتوای این مطلب لزوماً موردتأیید جاواکاپ نیست)

dependency injection یا inversion of control امروزه مفهومی آشنا برای اکثر جاوا دولوپر هاست .
dependency injection به یک کامپوننت اجازه میده تا reference از کامپوننتی دیگر رو با استفاده از کانتینر در خودش تزریق کنه .

در تقریبا تمام پیاده سازی های dependency injection مخصوصا پیاده سازی های سنتی ( اسپرینگ ) که شما تا حالا دیدید injection ( تزریق ) زمانیکه کامپوننت ساخته شده است ، رخ میدهد و reference در مدت طول عمر کامپوننت تغییری نمی کنه . فریم ورک جی باس سیم مفهوم جدیدی بنام bijection را معرفی می کند که بر خلاف injection :

  • کامپوننت ها با اسکوپ های کانتکست بزرگتر می توانند رفرنس به کامپوننت ها از کانتکست کوچکتر داشته باشند . این یعنی شما باید از خودتون بپرسید آیا در اسپرینگ می توان کامپوننتی با اسکوپ prototype را در کامپوننتی با اسکوپ singleton تزریق کرد ؟ اگر که مفهوم این دو را کامل بدانید جوابتان خیر است البته اسپرینگ کلی امکانات دیگه ای داره که شما می تونید دلتون رو به اون ها خوش کنید .
  • دوطرفه بودن تزریق : کامپوننت ها می توانند از کانتکس ، داخل متغیر ها تزریق شوند و پس از اعمال تغییر روی آن ها به بیرون پرتاب شوند .
  • پویایی : از اونجاییکه مقدار متغیرهای کانتکس در طول زمان تغییر می کند و کامپوننت های سیم stateful است bijection هر بار که یک کامپوننت اجرا می شه ، صدا زده می شود .

اجازه بدید جملات مبهم بالا رو با چند مثال توضیح بدم :

فرض کنید شما فرمی دارید که کاربر ، کد ملی خود را وارد می کند و فاکتورهایش را میبیند :

<h:form>
	<h:inputText value="#{user.nationalNo}" required="true" />
	<h:commandButton action="#{invoiceAction.showUserInvoice()}" value="Show Invoice"/>
</h:form>

<h:dataTable rendered="#{not empty user.invoices}" value="#{user.invoices}" var="_invoice">
	//baghiye dastan
</h:dataTable>

پس از کلیک بر روی دکمه مشاهده فاکنور .

@Name("invoiceAction")
@Scope(Conversation)
public class InvoiceActionBean 
{

	@In @Out User user;

	public void showUseInvoice()
	{
		List<Invoice> invoices = fetchUserInvoiceFromDatabase(user.getNationalNo());
		user.addInvoices(invoices);
	}

}

برخلاف سیم ، تزریق در اسپرینگ زمانیکه متد اجرا می شود رخ نمی دهد فقط زمانی تزریق اتفاق میفتد که bean اسپرینگ instantiate میشود .

یک مثال دیگر ، فرض کنید در یک فرم ثبت نام ، کاربر باید دو معرف را نیز همراه با ثبت نام وارد کند .

@Name("userAgentHome")
@Scope(Conversation)
@Role(name="agentHome", Scope=EVENT)
public class UserAgentHome extends EntityHome<UserAgent>{

	@In(create=true)
	UserAgentHome agentHome;

	public void save()
	{
		agentHome.persist();
		persist();
	}

} 

حالا سعی کنید در اسپرینگ یک کامپوننت را در خودش تزریق کنید یا حتی ساده تر از اون.

@Component
public class A
{
	@Autowired
	B b;
}


@Component
public class B
{
	@Autowired
	A a;
}

 

اسپرینگ به شما می گوید انقدر باهوش نیست که اینکار را انجام دهد و خطای Spring Circular Dependency exception .

در اپلیکیشن های اسپرینگ رسم است که حتی لایه های سرویس که منطق برنامه ( business logic ) در آن وجود دارد را اینترفیس میگیرند درحالیکه در برنامه نویسی شی گرا ، پرکاربردترین استفاده اینترفیس  زمانی است که پیاده سازی های مختلفی از یک مسئله وجود داشته باشد مگر اینکه در حال توسعه SPI باشید  مثلا  لایه دیتا اکسس ، طبیعی است که می تواند اینترفیس باشد چون پیاده سازی های مختلفی برای پایگاه داده های مختلف وجود دارد. bean های اسپرینگ  resolve by type است . به کد زیر دقت کنید .

@Component
public class BrowserUpload implements Upload{}

@Component
public class IEUpload implements Upload{}

@Autowired
Upload upload;

اسپرینگ از کجا میفهمد کدام کامپوننت آپلود منظور است ؟ عمرا نمیفهمد برای اینکار اسپرینگ یک annotation دیگری بنام Qualifier دارد. Seam Resolve by name است .

@Name("browserUpload")
public class BrowserUpload implements Upload{}

@Name("ieUpload")
public class IEUpload implements Upload{}

@In
Upload browserUpload;

قضیه به اینجا ختم نمیشه و اینها تنها  مواردی در قسمت DI فریم ورک اسپرینگ استو این موارد به اسپرینگ mvc هم قابل تعمیم است . فریم ورک اسپرینگ در جاوا راه حلی برای مشکلی است که آن مشکل دیگر وجود ندارد .

با توجه به اینکه این پست خیلی مورد عنایت و توجه قرار گرفت بروز رسانی ای در آن انجام شد .

 

کدوم دولوپری نیست که از دیدن چنین کدی خسته نشده باشه ؟!

public Order createOrder(User user, Product product, int quantity) {

    if ( log.isDebugEnabled() ) {

        log.debug("Creating new order for user: " + user.username() + 

            " product: " + product.name() 

            + " quantity: " + quantity);

    }

    return new Order(user, product, quantity);

}

تصورش سخته که برای یک لاگ ساده انقدر بخوای کد کثیف بزنی . کدی که برای لاگ زده شده از کدی که برای منطق برنامه نوشته شده بیشتره . آدم حیرت زده میشه که فریم ورکی مثل اسپرینگ بعد از ده سال هیچ راه حلی برای اینکار پیشنهاد نکرده . فریم ورک سیم API ای فراهم کرده که اینکار رو براحتی انجام بدید .

@Logger private Log log;

        

public Order createOrder(User user, Product product, int quantity) {

    log.debug("Creating new order for user: #0 product: #1 quantity: #2", user.username(), product.name(), quantity);

    return new Order(user, product, quantity);

}

از آنجاییکه من به هر دو فریم ورک مسلط هستم و از روش دوم بعنوان یک دولوپر حرفه ای بیشتر خوشم میاد این روش رو در اسپرینگ پیاده سازی می کنم ولی خیلی از دولوپر ها مثه من نیستن و از روش اول استفاده میکنن و وقتی که کد یکی از اون ها رو به شما میدن تا maintain کنید خودتون می تونید حدس بزنید چه اتفاقی میفته .

ابتدا یک annotation میسازیم

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface Logger {

}

برای فهمیدن بقیه کد به جاواداک اسپرینگ مراجعه کنید

public class LoggerFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
    {
        if (!(beanFactory instanceof DefaultListableBeanFactory))
        {
            throw new IllegalStateException("LoggerFactoryPostProcessor needs to operate on a DefaultListableBeanFactory");
        }
        DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
        dlbf.addBeanPostProcessor(new LoggerPostProcessor());

    }

}

public class LoggerPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
    {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException
    {
        ReflectionUtils.doWithFields(bean.getClass(), new FieldCallback() {
            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException
            {
                if (field.getAnnotation(Logger.class) != null)
                {
                    ReflectionUtils.makeAccessible(field);
                    org.slf4j.Logger log = LoggerFactory.getLogger(bean.getClass());
                    field.set(bean, log);
                }
            }
        });
        return bean;
    }

}

در فایل کانفیگ اسپرینگ

<bean class="com.core.logger.LoggerFactoryPostProcessor"></bean>

البته اسپرینگ از نسخه 4.3 به بعد فهمیدن که این امکان خوبه و مفهومی بنام injectionpoint اضافه کرد تا برای انجام همچین کار ساده ای خون دماغ نشید .

Component driven event

اگر با pattern observer/observable آشنا باشید نیاز به توضیح اضافی در این قسمت نیست . فرض کنید بعد از اجرای یک متد میخواهید یک رویدادی در یک متد کامپوننت دیگر صدا کنید . در قریم ورک سیم اینکار به اینصورت است .

@Name("helloWorld")

public class HelloWorld {

    @RaiseEvent("hello")

    public void sayHello() {

        FacesMessages.instance().add("Hello World!");

    }

}
@Name("helloListener")

public class HelloListener {

    @Observer("hello")
    public void sayHelloBack() {

        FacesMessages.instance().add("Hello to you too!");

    }

}

اسپرینگ امکان event رو به شما میده ولی استفاده از اون به این راحتی نیست ولی باز هم بعنوان یک دولوپر حرفه ای شما باید کدهای خوانا و قابل maintain تولید کنید و اینکار با فریم ورک اسپرینگ به این راحتی نیست برای پیاده سازی این امکان در اسپرینگ به صورت زیر عمل می کنیم . قبل از انجام اینکار باید با اسپرینگ AOP آشنا باشید .

 

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface RaiseEvent {

    String name();

    // TODO : use Spel for sending parameters
    String expression() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface Observer {

    String name();

}



public class SeamEvent extends ApplicationEvent {

    String name;
    Object[] parameters;

    public SeamEvent(Object source) {
        super(source);
    }

    
    public SeamEvent(Object source, String name, Object[] parameters) {
        super(source);
        this.name = name;
        this.parameters = parameters;
    }

    public String getName()
    {
        return name;
    }

    public Object[] getParameters()
    {
        return parameters;
    }

}

public class RaiseEventAdvice implements MethodBeforeAdvice, ApplicationEventPublisherAware {

    ApplicationEventPublisher eventPublisher;


    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable
    {
        if (method.isAnnotationPresent(RaiseEvent.class))
        {
            RaiseEvent raiseEvent = method.getAnnotation(RaiseEvent.class);
            String methodName = raiseEvent.name();
            eventPublisher.publishEvent(new SeamEvent(target, methodName, null));
        }
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher)
    {
        this.eventPublisher = applicationEventPublisher;
    }

}

public class EventManager implements BeanPostProcessor {

    private Multimap<String, Map<Object, Method>> observers = ArrayListMultimap.create();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
    {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException
    {
        ReflectionUtils.doWithMethods(bean.getClass(), new MethodCallback() {

            @Override
            public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException
            {
                if (method.isAnnotationPresent(Observer.class))
                {
                    Observer observer = method.getAnnotation(Observer.class);
                    Map<Object, Method> beanMaps = new HashMap<Object, Method>();
                    beanMaps.put(bean, method);
                    observers.put(observer.name(), beanMaps);
                }
            }
        });
        return bean;
    }

    public Multimap<String, Map<Object, Method>> getMethods()
    {
        return observers;
    }

}

تنظیمات در فایل کانفیگ اسپرینگ

<beans:bean id="raiseEventAdvice" class="com.core.aop.RaiseEventAdvice" />
	


	<beans:bean id="raiseEventAdvisor"
		class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
		<beans:property name="advice" ref="raiseEventAdvice" />
	</beans:bean>


	<beans:bean
		class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
		<beans:property name="proxyTargetClass" value="true"></beans:property>
		<beans:property name="beanNames">
			<beans:list>				
				<beans:value>*Service</beans:value>
			</beans:list>
		</beans:property>
		<beans:property name="interceptorNames">
			<beans:list>
				<beans:value>raiseEventAdvice</beans:value>				
			</beans:list>
		</beans:property>
	</beans:bean>

همانطور که میبیند AOP در عین حال که یکی از قدرت های اسپرینگ به شمار میرود یکی از ضعف های آن هم هست چراکه در اسپرینگ meta programming های زیادی وجود دارد اگر از google Guice استفاده کرده باشید از آنجاییکه از کانفیگ های xml خبری نیست شما میدانید دقیقا چه اتفاقی در حال رخ دادن است ( این بحث خارج از اسکوپ این مقاله است )

هر احمقی می تونه با اسپرینگ کد بزنه و در نهایت شاید یک نرم افزار تولید کنه ولی هر کسی نمی تونه با این فریم ورک ، نرم افزاری قابل maintain با کدهای خوانا و معماری خوب تولید کنه .

اسپرینگ شاید فریم ورک خوبی باشه ولی فقط برای اون هاییکه درست بلدن از این ابزار استفاده کنن . شما باید از خودتان بپرسید که یک فریم ورک  چقدر در تولید یک نرم افزار با معماری خوب به کمکتان می آید ؟!

 

فریم ورک اسپرینگ معماری را به شما تحمیل می کند

فرض کنید در لایه سرویس چنین متدی داریم که چند رکورد در پایگاه داده insert می کنه و بعدش در همون متد می خوایم لیستی از رکوردهای insert شده را برگردونیم

 

@PersistenceContext
EntityManager entityManager;

@Transactional
public void processModels()
{
        entityManager.createQuery("delete from Customer c where c.createdTimestamp <> :date").setParameter("date", d).executeUpdate();
	entityManager.flush();
	entityManager.persist(customer1);
	entityManager.persist(customer2);
	entityManager.persist(customer3);
	entityManager.flush();
	List customers = jdbTemplate.query("select * from customer c where created_timestamp = ?", d);
        System.out.println(customers.size());
}

اگر کد بالا را در لایه سرویس اجرا کنید از آنجاییکه اسپرینگ فقط در سطح متد تراکنش داره یا بهتر بگم entityManager رو proxy میکنه به SharedEntityManagerBean با وجود اینکه شما flush کرده اید و سه تا مشتری insert کردید لیست شما خالی خواهد بود و مجبور می شید تا کوئری native را در یک متد جداگانه کال کنید .

اگر هم بخواهید اسپرینگ را مجبور کنید که تراکنش رو بدست شما بسپاره با خطای زیر مواجه می شوید .

entityManager.getTransaction().commit();

Not allowed to create transaction on shared EntityManager use Spring transactions or EJB CMT instead

درسته که کد نمونه بد نوشته شده اما رفتارهای غیرمنتظرانه این فریم ورک همیشه شما را شگفت زده خواهد کرد .

نوشته های مشابه

‫7 دیدگاه ها

  1. سلام
    متن مطلب کاملا گواهی میده که شما به اسپرینگ مسلط نیستید
    به نظرم شما اگه اسپرینگ رو مسلط بشید نظرتون کامل تغییر می کنه

  2. فریم ورک اسپرینگ کلی تو دنیا طرفدار داره – کلی از پروژه ها دارن باهاش ران میشن ، اگه به وب سایتهای کاریابی خارجی مثل مانستر و… هم سر بزنید میبینید که فریم ورک اسپرینگ بخش جدایی ناپذیر نیازمندی های برنامه نویس جاوا هستش ، الان منظورتون چیه ، به نظرتون عمر اسپرینگ به سر اومده یا شرکت ها به سمت سیم مهاجرت کنند ؟ خیلی از شرکت های قدیمی غربی خصوصا توی اروپا وقتی میبینن تکنولوژی که استفاده میکنن و اگه قدیمی هم حتی شده باشه ولی اگه سیستمشون همچنان بالا باشه ازش استفاده میکنن و به زور رو میارن به چیزای جدیدتر مثل اسپرینگ بوت و… و نمیان دوباره کلی پول خرج کنن تا پروژه خودشون رو بیارن روی یه فریم ورک جدیدتر ، توی انگلیس من خودم پروژه هایی رو دیدم که به EJB 2.1, final درست شده بودن و هنوز هم سرویس میدادن

  3. «فریم ورک اسپرینگ در جاوا راه حلی برای مشکلی است که آن مشکل دیگر وجود ندارد .» یعنی چی ؟؟ میشه دقیق تر توضیح بدید …

  4. سلام،
    با تجربه ای که دارم، همانطور که حسام اشاره کرد، مشکلاتی که فرمودید در اسپرینگ وجود ندارد. من فقط یک نکته اضافه کنم که در بحث inject کردن, اسپرینگ هم resolve by type، هست و هم resolve by name.

  5. ضمن تشکر از مقاله تون باید عرض کنم که در این مطلب یک سری ادعا پشت سر هم مطرح شده است که نیاز به توضیح و شفاف سازی بیشتری دارد.
    Inject کردن prototype در singleton در spring به شکلی که شما مطرح کردید بدون دخالت Container و از طریق proxy قابل انجام است (مثل Inject کردن EntityManager در JPA یا Request در JaxRS). راستش این بحث یک مقدار به معماری فریم ورک و Application وابسته است. ما در Spring و معماری restful خیلی نیازی به این مسئله حس نمی کنیم. مثال هایی که شما زدید همه Inject کردن ورودی هایی است که از سمت کاربر آمده است و این موارد در Spring MVC از طریق متود، inject می شود. فکر نمی کنم از دیدگاه عملکردی تفاوتی وجود داشته باشد و بیشتر ناشی از تفاوت رویکرد است. (Request Based vs Component Based Frameworks)
    بحث circular dependency نیز در spring ممکن است و مشکلی ندارد. فقط زمانی که dependency ها در constructor تعریف شده باشند خطا می دهد که منطقی به نظر می رسد.

پاسخ دادن به ramin لغو پاسخ

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا