دومین مثال عملی که اجرا خواهیم کرد، طراحی یک Framework ساده Dependency Injection است. این سیستمی است که در Java EE، Jakarta EE و سایر Framework های کاربردی سازمانی وجود دارد. مفهوم ساده است: بهجای اینکه خودتان اشیای مورد نیاز برنامه را ایجاد کنید، این وظیفه را به یک Factory واگذار میکنید. این Factory میتواند بهصورت Reflective کلاس شما را کاوش کند، بررسی کند آیا فیلدهایی نیاز به مقداردهی اولیه دارند و در صورت نیاز این کار را برای شما انجام دهد.
Dependency Injection در برنامهها استفاده میشود تا اطمینان حاصل شود تمام اشیای Business شما بهدرستی مقداردهی اولیه شدهاند و تمام فیلدهایشان مقدار صحیح دریافت میکنند. مقداردهی اولیه مجموعهای از اشیای Business میتواند پیچیده باشد، چون اشیای شما به اشیای همکار (Collaborator Objects) وابسته هستند که باید به ترتیب خاصی بهدرستی مقداردهی اولیه شوند.
این بخش دو قابلیتی که این Framework ها ارائه میدهند را نشان میدهد. اولین مفهوم Singleton و دومی خود Dependency Injection است.
بیایید ابتدا یک Bean Factory ساده پیادهسازی کنیم. یعنی Factoryای که با الگوی زیر استفاده کنید:
public record Message(String message) {
public Message {
Objects.requireNonNull(message);
}
}
void main() {
BeanFactory beanFactory = BeanFactory.INSTANCE;
Message message1 = beanFactory.getInstanceOf(Message.class, "Hello");
IO.println("Message 1 = " + message1);
Message message2 = beanFactory.getInstanceOf(Message.class, "world");
IO.println("Message 2 = " + message2);
}
این کلاس الگوی Singleton را پیادهسازی میکند که بهسادگی با یک Enum قابل پیادهسازی است. متود getInstanceOf() یک کلاس (Typeای که میخواهید نمونهاش را بسازید) و آرگومانهای Constructor را میگیرد.
پیادهسازی کلاس BeanFactory ساده است. ابتدا آرایهای از Type آرگومانها ایجاد میشود، سپس Constructor مربوطه شناسایی شده و با آرگومانها فراخوانی میشود:
public enum BeanFactory {
INSTANCE;
public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
try {
// Creating the array of the parameters types
Class<?>[] argumentsClasses =
Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
// Locating the corresponding constructor
Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
// creating the bean
T bean = beanConstructor.newInstance(arguments);
return bean;
} catch (NoSuchMethodException | InvocationTargetException |
InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
نکته مهم: برای اینکه
getConstructor()Constructor مورد نظر را پیدا کند، Type ها باید دقیقاً یکسان باشند. اگر Constructor بهجایStringپارامترCharSequenceبگیرد، فراخوانیbeanFactory.getInstanceOf(Message.class, "Hello")خطایNoSuchMethodExceptionپرتاب میکند.
خروجی:
Message 1 = Message[message=Hello]
Message 2 = Message[message=world]
الگوی بسیار رایجی که در برنامههای سازمانی استفاده میشود، ایجاد Singleton توسط Factory است. این منطقی است اگر Bean های شما برای دسترسی به Service هایی مانند پایگاه داده یا سرور REST استفاده شوند.
ابتدا Annotation مورد نیاز را تعریف کنید:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Singleton {
}
و یک کلاس Service ایجاد کنید:
@Singleton
public class DBService {
}
BeanFactory باید بازسازی (Refactor) شود تا Annotation @Singleton را تشخیص دهد و در آن صورت فقط یک نمونه از آن کلاس ایجاد کند. این بخش نیاز به یک Registry دارد که باید Thread-Safe باشد.
اگر Registry قبلاً نمونهای از beanClass داشته باشد، همان نمونه بازمیگردد. در غیر این صورت نمونه جدیدی ایجاد میشود و به Registry اضافه میشود.
نکته مهم: فراخوانی
ConcurrentMap.putIfAbsent()یک عملیات اتمی (Atomic) است. اما ممکن است مقداری که پاس دادهاید دقیقاً همان مقداری نباشد که در نهایت در Map قرار گرفته — بهخصوص اگر چند Thread همزمان این متود را با نمونههای مختلف فراخوانی کنند. به همین دلیل بایدConcurrentMap.get()را فراخوانی کنید تا مقدار نهایی Singleton دریافت شود.
public enum BeanFactory {
INSTANCE;
private final ConcurrentMap<Class<?>, Object> registry = new ConcurrentHashMap<>();
public <T> T getInstanceOf(Class<T> beanClass, Object... arguments) {
try {
// Checking if the @Singleton annotation in on beanClass
if (beanClass.isAnnotationPresent(Singleton.class)) {
// Checking if the registry has an instance beanClass
if (registry.containsKey(beanClass)) {
return (T) registry.get(beanClass);
}
// Creating a new instance of beanClass
T bean = instantiateBeanClass(beanClass, arguments);
// Adding this instance to the registry
// putIfAbsent() is atomic in the case of a ConcurrentMap
registry.putIfAbsent(beanClass, bean);
// Returning the value that landed in the map
// It could be another one than yours
return (T) registry.get(beanClass);
} else {
T bean = instantiateBeanClass(beanClass, arguments);
return bean;
}
} catch (NoSuchMethodException | InvocationTargetException |
InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
throws NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
Class<?>[] argumentsClasses =
Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
T bean = beanConstructor.newInstance(arguments);
return bean;
}
}
خروجی اجرای متود main():
Message = Message[message=Hello]
Instances of DBService are the same? true
آخرین مرحله فعالسازی اسکن beanClass برای جستجوی فیلدهای Annotationدار است.
ابتدا Annotation زیر را تعریف کنید:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Inject {
}
و Service زیر را تعریف کنید که به DBService وابسته است:
@Singleton
public class MyApplication {
@Inject
private DBService dbService;
public boolean isDBServiceSet() {
return dbService != null;
}
}
حالا Factory باید نمونه صحیح DBService را در فیلد مناسب MyApplication قرار دهد. برای این کار باید کدی به متود instantiateBeanClass() اضافه شود تا فیلدها اسکن شوند و فیلدهایی که Annotation @Inject دارند شناسایی شوند.
در مرحله اول، bean از beanClass و arguments ساخته میشود. در این مرحله یک bean دارید که تمام فیلدهایش null هستند.
مرحله بعد دریافت تمام فیلدهای declared شده این bean است (بدون توجه به Modifier های قابلیت دید) و نگهداشتن فیلدهایی که Annotation @Inject دارند.
سپس برای هر یک از این فیلدهای Annotationدار، Type صحیح نمونهسازی شده و فیلد مقداردهی میشود. توجه کنید نمونهسازی از BeanFactory استفاده میکند، پس اعلان @Singleton رعایت میشود:
private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
throws NoSuchMethodException, InstantiationException,
IllegalAccessException, InvocationTargetException {
Class<?>[] argumentsClasses =
Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
// This is the constructed bean
T bean = beanConstructor.newInstance(arguments);
// Getting the fields of this bean
Field[] fields = beanClass.getDeclaredFields();
// Filtering the fields that declare the @Inject annotation
Field[] injectableFields =
Arrays.stream(fields)
.filter(field -> field.isAnnotationPresent(Inject.class))
.toArray(Field[]::new);
for (Field injectableField : injectableFields) {
// Getting the class of this field,
// and creating an instance of this class
// using BeanFactory
Class<?> fieldClass = injectableField.getType();
Object fieldValue = BeanFactory.INSTANCE.getInstanceOf(fieldClass);
// Setting this field to this value
injectableField.setAccessible(true);
injectableField.set(bean, fieldValue);
}
return bean;
}
و کد زیر را برای بررسی اینکه همه چیز بهدرستی مقداردهی شده اجرا کنید:
void main() {
BeanFactory beanFactory = BeanFactory.INSTANCE;
MyApplication app = BeanFactory.INSTANCE.getInstanceOf(MyApplication.class);
IO.println("App has been created with a DBService: " + app.isDBServiceSet());
}
App has been created with a DBService: true
همچنین میتوانید مثال زیر را اجرا کنید:
void main() {
BeanFactory beanFactory = BeanFactory.INSTANCE;
Message message = beanFactory.getInstanceOf(Message.class, "Hello");
IO.println("Message = " + message);
DBService dbService1 = beanFactory.getInstanceOf(DBService.class);
DBService dbService2 = beanFactory.getInstanceOf(DBService.class);
IO.println("Instances of DBService are the same? " + (dbService1 == dbService2));
}
Message = Message[message=Hello]
Instances of DBService are the same? true
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/