في هذه المقالة ، أريد مشاركة الملاحظات حول بعض مضادات الأضداد الموجودة في رمز التطبيقات التي تعمل في Spring. كلهم ظهروا بطريقة أو بأخرى في الكود المباشر: إما صادفتهم في الفصول الموجودة ، أو اشتعلت أثناء قراءة عمل الزملاء.
آمل أن تكونوا مهتمين ، وإذا كنت تقرأ "خطاياك" وتقرع في طريق التصحيح ، فسوف أكون مسرورًا على نحو مضاعف. وأحثك أيضًا على مشاركة الأمثلة الخاصة بك في التعليق ، وسوف نضيف الأكثر فضولية وغير عادية لهذا المنصب.
Autowired
إن @Autowired
العظيم @Autowired
حقبة كاملة في الربيع. لا يزال لا يمكنك الاستغناء عنها عند كتابة الاختبارات ، ولكن في الكود الرئيسي (PMSM) لا لزوم له. في العديد من مشاريعي الأخيرة ، لم يكن على الإطلاق. لفترة طويلة كتبنا مثل هذا:
@Component public class MyService { @Autowired private ServiceDependency; @Autowired private AnotherServiceDependency;
سبق وصف أسباب تفصيل حقن التبعية عبر الحقول والمستوطنين بالتفصيل ، على وجه الخصوص هنا . البديل هو التنفيذ من خلال المنشئ. باتباع الرابط ، تم توضيح مثال:
private DependencyA dependencyA; private DependencyB dependencyB; private DependencyC dependencyC; @Autowired public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) { this.dependencyA = dependencyA; this.dependencyB = dependencyB; this.dependencyC = dependencyC; }
يبدو لائقًا إلى حد ما ، ولكن تخيل أن لدينا 10 تبعيات (نعم ، نعم ، أعلم أنه في هذه الحالة يجب أن يتم تجميعهم في فصول منفصلة ، ولكن الآن لا يتعلق الأمر بذلك). الصورة لم تعد جذابة للغاية:
private DependencyA dependencyA; private DependencyB dependencyB; private DependencyC dependencyC; private DependencyD dependencyD; private DependencyE dependencyE; private DependencyF dependencyF; private DependencyG dependencyG; private DependencyH dependencyH; private DependencyI dependencyI; private DependencyJ dependencyJ; @Autowired public DI() { this.dependencyA = dependencyA; this.dependencyB = dependencyB; this.dependencyC = dependencyC; this.dependencyD = dependencyD; this.dependencyE = dependencyE; this.dependencyF = dependencyF; this.dependencyG = dependencyG; this.dependencyH = dependencyH; this.dependencyI = dependencyI; this.dependencyJ = dependencyJ; }
رمز بصراحة ، يبدو وحشية.
وهنا ينسى الكثيرون هنا أيضًا عازف الكمان ليست هناك حاجة @Autowired
! إذا كان لدى الفصل مُنشئ واحد فقط ، فسوف يفهم Spring (> = 4) أن التبعيات تحتاج إلى تنفيذها من خلال هذا المُنشئ. حتى نتمكن من رميها بعيدًا ، واستبدالها بـ @AllArgsContructor
. أو حتى أفضل - على @RequiredArgsContructor
، دون أن ننسى الإعلان عن جميع الحقول الضرورية final
وتلقي تهيئة آمنة للكائن في بيئة متعددة الخيوط (بشرط أن تتم تهيئة جميع التبعيات بأمان):
@RequiredArgsConstructor public class DI { private final DependencyA dependencyA; private final DependencyB dependencyB; private final DependencyC dependencyC; private final DependencyD dependencyD; private final DependencyE dependencyE; private final DependencyF dependencyF; private final DependencyG dependencyG; private final DependencyH dependencyH; private final DependencyI dependencyI; private final DependencyJ dependencyJ; }
طرق ثابتة في فئات الأداة المساعدة ووظائف التعداد
غالبًا ما تكون Bloody E مهمة تحويل كائنات حامل البيانات من طبقة تطبيق إلى كائنات مماثلة لطبقة أخرى. لهذا ، لا تزال تستخدم فئات الأداة المساعدة مع أساليب ثابتة مثل (تذكر ، في عام 2019):
@UtilityClass public class Utils { public static UserDto entityToDto(UserEntity user) {
يدرك المستخدمون الأكثر تقدماً الذين يقرؤون الكتب الذكية الخصائص السحرية للتعدادات:
enum Function implements Function<UserEntity, UserDto> { INST; @Override public UserDto apply(UserEntity user) {
صحيح ، في هذه الحالة ، لا تزال المكالمة تحدث لكائن واحد ، وليس لعنصر يتحكم فيه Spring.
يعرف المزيد من الرجال (والفتيات) المتقدمين عن MapStruct ، والذي يسمح لك بوصف كل شيء في واجهة واحدة:
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) public interface CriminalRecommendationMapper { UserDto map(UserEntity user); }
الآن نحصل على عنصر الربيع. يبدو مثل النصر. لكن الشيطان هو في التفاصيل ، ويحدث أن النصر يصبح ساحقا. أولاً ، يجب أن تكون أسماء الحقول هي نفسها (بخلاف ذلك تبدأ البواسير) ، وهي ليست مريحة دائمًا ، وثانياً ، في حالة وجود أي تحويلات حقل معقدة للكائنات المعالجة ، تنشأ صعوبات إضافية. حسنًا ، يحتاج mapstruct بحد ذاته إلى إضافته وفقًا.
يتذكر عدد قليل من الناس الطريقة القديمة ، ولكن مع ذلك بسيطة وعملية للحصول على محول يحركه الربيع:
import org.springframework.core.convert.converter.Converter; @Component public class UserEntityToDto implements Converter<UserEntity, UserDto> { @Override public UserDto convert(UserEntity user) {
الميزة هنا هي أنه في فصل آخر ، أنا فقط بحاجة للكتابة
@Component @RequiredArgsConstructor public class DI { private final Converter<UserEntity, UserDto> userEnityToDto; }
والربيع سوف يحل كل شيء بمفرده!
تصفيات النفايات
حالة الحياة: يعمل التطبيق مع اثنين من قواعد البيانات. وفقًا لذلك ، يوجد مصدران للبيانات ( java.sql.DataSource
) ، java.sql.DataSource
للمعاملات ، ومجموعتان من المستودعات ، إلخ. كل هذا موصوف بشكل مريح في إعدادين منفصلين. هذا من أجل Postgre:
@Configuration public class PsqlDatasourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.psql") public DataSource psqlDataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase primaryLiquibase( ProfileChecker checker, @Qualifier("psqlDataSource") DataSource dataSource ) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog("classpath:liquibase/schema-postgre.xml"); liquibase.setShouldRun(isTest); return liquibase; } }
وهذا هو ل DB2:
@Configuration public class Db2DatasourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase liquibase( ProfileChecker checker, @Qualifier("db2DataSource") DataSource dataSource ) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(dataSource); liquibase.setChangeLog("classpath:liquibase/schema-db2.xml"); liquibase.setShouldRun(isTest); return liquibase; } }
نظرًا لأن لديّ قاعدتي بيانات ، بالنسبة للاختبارات ، أرغب في تشغيل قاعدتي DDL / DML منفصلتين عليها. نظرًا لأن كلا @Qualifier
في نفس الوقت الذي يتم فيه تشغيل التطبيق ، إذا قمت @Qualifier
، @Qualifier
Spring تعيينه المستهدف وسيفشل في أحسن الأحوال. اتضح أن @Qualifier
مرهقة وعرضة للخدوش ، وبدونها لا يعمل. لكسر الجمود ، يجب أن تدرك أنه يمكن الحصول على التبعية ليس فقط كوسيطة ، ولكن أيضًا كقيمة إرجاع ، عن طريق إعادة كتابة الكود مثل هذا:
@Configuration public class PsqlDatasourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.psql") public DataSource psqlDataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase primaryLiquibase(ProfileChecker checker) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(psqlDataSource());
javax.inject.Provider
كيفية الحصول على الفول مع نطاق النموذج الأولي؟ كثيرا ما جئت عبر هذا
@Component @Scope(SCOPE_PROTOTYPE) @RequiredArgsConstructor public class ProjectBuilder { private final ProjectFileConverter converter; private final ProjectRepository projectRepository;
يبدو أن كل شيء على ما يرام ، رمز يعمل. ومع ذلك ، في برميل العسل هذا هناك ذبابة في المرهم. نحتاج إلى سحب javax.inject:javax.inject:1
تبعية javax.inject:javax.inject:1
، والتي تمت إضافتها إلى Maven Central تمامًا قبل 10 سنوات ولم يتم تحديثها منذ ذلك الحين.
لكن الربيع كان قادرًا على فعل الشيء نفسه بدون إدمان الطرف الثالث! فقط javax.inject.Provider::get
with org.springframework.beans.factory.ObjectFactory::getObject
وكل شيء يعمل بنفس الطريقة.
@Component @RequiredArgsConstructor public class PrototypeUtilizer { private final ObjectFactory<ProjectBuilder> projectBuilderFactory; void method() { ProjectBuilder freshBuilder = projectBuilderFactory.getObject(); } }
الآن يمكننا ، مع ضمير واضح ، قطع javax.inject
من قائمة التبعيات.
استخدام السلاسل بدلاً من الفئات في الإعدادات
مثال شائع لربط مستودعات Spring Data بمشروع:
@Configuration @EnableJpaRepositories("com.smth.repository") public class JpaConfig {
هنا نصف صراحة الحزمة التي سيتم عرضها بواسطة Spring. إذا سمحنا بتسمية إضافية ، فسيتعطل التطبيق عند بدء التشغيل. أود اكتشاف هذه الأخطاء الغبية في المراحل المبكرة ، في الحد الأقصى - أثناء تحرير الكود. يتجه الإطار نحونا ، لذلك يمكن إعادة كتابة الكود أعلاه:
@Configuration @EnableJpaRepositories(basePackageClasses = AuditRepository.class) public class JpaConfig {
هنا AuditRepository
هي واحدة من مستودعات الحزمة التي AuditRepository
. نظرًا لأننا أشرنا إلى الفصل ، سنحتاج إلى توصيل هذه الفئة بتكويننا ، وسيتم الآن اكتشاف الأخطاء المطبعية مباشرةً في المحرر أو ، في أسوأ الأحوال ، عند إنشاء المشروع.
يمكن تطبيق هذا النهج في العديد من الحالات المماثلة ، على سبيل المثال:
@ComponentScan(basePackages = "com.smth")
يتحول إلى
import com.smth.Smth; @ComponentScan(basePackageClasses = Smth.class)
إذا كنا بحاجة إلى إضافة فئة إلى قاموس النموذج Map<String, Object>
، فيمكن القيام بذلك على النحو التالي:
void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, "com.smth.interceptor.AuditInterceptor"); }
لكن من الأفضل استخدام نوع صريح:
import com.smth.interceptor.AuditInterceptor; void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, AuditInterceptor.class); }
وعندما يكون هناك شيء مثل
LocalContainerEntityManagerFactoryBean bean = builder .dataSource(dataSource) .packages(
تجدر الإشارة إلى أن طريقة packages()
محمّلة بأكثر من اللازم وتستخدم الفئات:

لا تضع كل الفاصوليا في عبوة واحدة
أعتقد في العديد من المشاريع في Spring / Spring Booth أنك رأيت بنية مماثلة:
root-package | \ repository/ entity/ service/ Application.java
هنا Application.java
هي فئة الجذر للتطبيق:
@SpringBootApplication @EnableJpaRepositories(basePackageClasses = SomeRepository.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
هذا هو رمز microservice الكلاسيكي: يتم ترتيب المكونات في مجلدات وفقًا للغرض ، والفئة مع الإعدادات هي في الجذر. في حين أن المشروع صغير ، فكل شيء على ما يرام. مع نمو المشروع ، تظهر حزم الدهون مع العشرات من المستودعات / الخدمات. وإذا كان المشروع لا يزال متراصة ، ثم Gd معهم. ولكن إذا نشأت المهمة لتقسيم التطبيق الخشنة إلى أجزاء ، فسيبدأ السؤال. بعد أن عانيت من هذا الألم مرة واحدة ، قررت اتباع نهج مختلف ، ألا وهو فئات المجموعة حسب مجالها. والنتيجة شيء من هذا القبيل
root-package/ | \ user/ | \ repository/ domain/ service/ controller/ UserConfig.java billing/ | \ repository/ domain/ service/ BillingConfig.java //... Application.java
هنا ، تتضمن حزمة user
حزم فرعية مع فئات مسؤولة عن منطق المستخدم:
user/ | \ repository/ UserRepository.java domain/ UserEntity.java service/ UserService.java controller/ UserController.java UserConfig.java
الآن في UserConfig
يمكنك وصف جميع الإعدادات المرتبطة بهذه الوظيفة:
@Configuration @ComponentScan(basePackageClasses = UserServiceImpl.class) @EnableJpaRepositories(basePackageClasses = UserRepository.class) class UserConfig { }
ميزة هذا النهج أنه ، إذا لزم الأمر ، يمكن بسهولة تخصيص الوحدات النمطية للخدمات / التطبيقات المنفصلة. كما أنه مفيد إذا كنت تنوي تنظيم مشروعك عن طريق إضافة module-info.java
، إخفاء فئات الأدوات المساعدة من العالم الخارجي.
هذا كل ما آمل أن يكون عملي مفيدًا لك. صف مضاداتك في التعليقات ، وسنقوم بتصنيفها معًا :)