ماذا نفعل خطأ مع الربيع

في هذه المقالة ، أريد مشاركة الملاحظات حول بعض مضادات الأضداد الموجودة في رمز التطبيقات التي تعمل في 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()); // <----- liquibase.setChangeLog("classpath:liquibase/schema-postgre.xml"); liquibase.setShouldRun(isTest); return liquibase; } } //... @Configuration public class Db2DatasourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2DataSource() { return DataSourceBuilder.create().build(); } @Bean public SpringLiquibase liquibase(ProfileChecker checker) { boolean isTest = checker.isTest(); SpringLiquibase liquibase = new SpringLiquibase(); liquibase.setDataSource(db2DataSource()); // <----- liquibase.setChangeLog("classpath:liquibase/schema-db2.xml"); liquibase.setShouldRun(isTest); return liquibase; } } 

javax.inject.Provider


كيفية الحصول على الفول مع نطاق النموذج الأولي؟ كثيرا ما جئت عبر هذا


 @Component @Scope(SCOPE_PROTOTYPE) @RequiredArgsConstructor public class ProjectBuilder { private final ProjectFileConverter converter; private final ProjectRepository projectRepository; //... } @Component @RequiredArgsConstructor public class PrototypeUtilizer { private final Provider<ProjectBuilder> projectBuilderProvider; void method() { ProjectBuilder freshBuilder = projectBuilderProvider.get(); } } 

يبدو أن كل شيء على ما يرام ، رمز يعمل. ومع ذلك ، في برميل العسل هذا هناك ذبابة في المرهم. نحتاج إلى سحب 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( //...     ) .persistenceUnit("psql") .build(); 

تجدر الإشارة إلى أن طريقة 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 ، إخفاء فئات الأدوات المساعدة من العالم الخارجي.


هذا كل ما آمل أن يكون عملي مفيدًا لك. صف مضاداتك في التعليقات ، وسنقوم بتصنيفها معًا :)

Source: https://habr.com/ru/post/ar472994/


All Articles