Apa yang kita lakukan salah dengan Spring

Pada artikel ini saya ingin berbagi pengamatan tentang beberapa antipatterns yang ditemukan dalam kode aplikasi yang berjalan di Spring. Semuanya dengan satu atau lain cara muncul di kode hidup: entah saya menemukan mereka di kelas yang ada, atau saya tertangkap saat membaca karya rekan kerja.


Saya harap Anda akan tertarik, dan jika setelah membaca Anda mengakui "dosa" Anda dan memulai jalan koreksi, saya akan sangat senang. Saya juga mendesak Anda untuk membagikan contoh Anda sendiri di komentar, kami akan menambahkan yang paling aneh dan tidak biasa ke posting.


Autowired


@Autowired hebat dan mengerikan adalah seluruh era di Musim Semi. Anda masih tidak dapat melakukannya tanpa itu ketika menulis tes, tetapi dalam kode utama itu (PMSM) jelas berlebihan. Dalam beberapa proyek terakhir saya, dia sama sekali tidak. Untuk waktu yang lama kami menulis seperti ini:


 @Component public class MyService { @Autowired private ServiceDependency; @Autowired private AnotherServiceDependency; //... } 

Alasan mengapa injeksi ketergantungan melalui ladang dan setter dikritik telah dijelaskan secara rinci, khususnya di sini . Alternatif adalah implementasi melalui konstruktor. Mengikuti tautan, sebuah contoh dijelaskan:


 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; } 

Kelihatannya kurang lebih layak, tetapi bayangkan kita memiliki 10 dependensi (ya, ya, saya tahu bahwa dalam hal ini mereka perlu dikelompokkan ke dalam kelas yang terpisah, tetapi sekarang ini bukan tentang itu). Gambar tidak lagi begitu menarik:


 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; } 

Kode itu terus terang, itu terlihat mengerikan.


Dan di sini, banyak yang lupa itu di sini juga pemain biola @Autowired tidak diperlukan! Jika sebuah kelas hanya memiliki satu konstruktor, maka Spring (> = 4) akan memahami bahwa dependensi perlu diimplementasikan melalui konstruktor ini. Jadi kita bisa membuangnya, menggantinya dengan Lombok @AllArgsContructor . Atau bahkan lebih baik - pada @RequiredArgsContructor , tanpa lupa menyatakan semua bidang yang diperlukan final dan menerima inisialisasi objek yang aman dalam lingkungan multi-berulir (asalkan semua dependensi juga diinisialisasi dengan aman):


 @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; } 

Metode statis di kelas utilitas dan fungsi enum


Bloody E sering memiliki tugas untuk mengkonversi objek data dari satu lapisan aplikasi ke objek serupa dari lapisan lain. Untuk ini, kelas utilitas dengan metode statis seperti ini masih digunakan (ingat, pada tahun 2019):


 @UtilityClass public class Utils { public static UserDto entityToDto(UserEntity user) { //... } } 

Pengguna yang lebih maju yang membaca buku pintar menyadari sifat magis enumerasi:


 enum Function implements Function<UserEntity, UserDto> { INST; @Override public UserDto apply(UserEntity user) { //... } } 

Benar, dalam hal ini, panggilan masih terjadi ke satu objek, dan bukan ke komponen yang dikendalikan oleh Spring.
Bahkan cowok yang lebih mahir (dan perempuan) tahu tentang MapStruct , yang memungkinkan Anda untuk mendeskripsikan semuanya dalam satu antarmuka:


 @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) public interface CriminalRecommendationMapper { UserDto map(UserEntity user); } 

Sekarang kita mendapatkan komponen pegas. Sepertinya kemenangan. Tetapi iblis ada dalam perinciannya, dan kebetulan kemenangan menjadi luar biasa. Pertama, nama bidang harus sama (jika wasir mulai), yang tidak selalu nyaman, dan kedua, jika ada transformasi bidang kompleks dari objek yang diproses, kesulitan tambahan muncul. Nah, mapstruct itu sendiri perlu ditambahkan tergantung.


Dan sedikit orang yang mengingat cara kuno, tetapi tetap sederhana dan bekerja untuk mendapatkan konverter pegas:


 import org.springframework.core.convert.converter.Converter; @Component public class UserEntityToDto implements Converter<UserEntity, UserDto> { @Override public UserDto convert(UserEntity user) { //... } } 

Keuntungannya di sini adalah di kelas lain, saya hanya perlu menulis


 @Component @RequiredArgsConstructor public class DI { private final Converter<UserEntity, UserDto> userEnityToDto; } 

dan Spring akan menyelesaikan semuanya sendiri!


Kualifikasi Limbah


Kasus hidup: aplikasi bekerja dengan dua basis data. Dengan demikian, ada dua sumber data ( java.sql.DataSource ), dua manajer transaksi, dua kelompok repositori, dll. Semua ini mudah dijelaskan dalam dua pengaturan terpisah. Ini untuk 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; } } 

Dan ini untuk 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; } } 

Karena saya memiliki dua database, untuk pengujian saya ingin menggulung dua DDL / DML terpisah. Karena kedua konfigurasi dimuat pada saat yang sama ketika aplikasi habis, jika saya @Qualifier , maka Spring akan kehilangan tujuan penargetannya dan, paling-paling, akan gagal. Ternyata @Qualifier rumit dan rentan terhadap goresan, dan tanpa mereka tidak akan berhasil. Untuk memecahkan kebuntuan, Anda perlu menyadari bahwa ketergantungan dapat diperoleh tidak hanya sebagai argumen, tetapi juga sebagai nilai balik, dengan menulis ulang kode seperti ini:


 @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


Bagaimana cara mendapatkan kacang dengan lingkup prototipe? Saya sering menemukan ini


 @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(); } } 

Tampaknya semuanya baik-baik saja, kodenya berfungsi. Namun, dalam tong madu ini ada lalat di salep. Kita perlu menyeret satu lagi javax.inject:javax.inject:1 dependensi, yang ditambahkan ke Maven Central tepat 10 tahun yang lalu dan tidak pernah diperbarui sejak saat itu.


Tetapi Spring telah lama dapat melakukan hal yang sama tanpa kecanduan pihak ketiga! Cukup ganti javax.inject.Provider::get dengan org.springframework.beans.factory.ObjectFactory::getObject dan semuanya bekerja dengan cara yang sama.


 @Component @RequiredArgsConstructor public class PrototypeUtilizer { private final ObjectFactory<ProjectBuilder> projectBuilderFactory; void method() { ProjectBuilder freshBuilder = projectBuilderFactory.getObject(); } } 

Sekarang kita dapat, dengan hati nurani yang jelas, memotong javax.inject dari daftar dependensi.


Menggunakan string sebagai ganti kelas dalam pengaturan


Contoh umum menghubungkan repositori Data Spring ke proyek:


 @Configuration @EnableJpaRepositories("com.smth.repository") public class JpaConfig { //... } 

Di sini kami secara eksplisit meresepkan paket yang akan dilihat oleh Spring. Jika kami mengizinkan sedikit penamaan tambahan, maka aplikasi akan macet saat startup. Saya ingin mendeteksi kesalahan bodoh seperti itu pada tahap awal, dalam batas - tepat saat mengedit kode. Kerangka kerja menuju kita, sehingga kode di atas dapat ditulis ulang:


 @Configuration @EnableJpaRepositories(basePackageClasses = AuditRepository.class) public class JpaConfig { //... } 

Di sini AuditRepository adalah salah satu repositori paket yang akan kita lihat. Karena kita menunjukkan kelas, kita perlu menghubungkan kelas ini ke konfigurasi kita, dan sekarang kesalahan ketik akan dideteksi secara langsung di editor atau, paling buruk, ketika membangun proyek.


Pendekatan ini dapat diterapkan dalam banyak kasus serupa, misalnya:


 @ComponentScan(basePackages = "com.smth") 

berubah menjadi


 import com.smth.Smth; @ComponentScan(basePackageClasses = Smth.class) 

Jika kita perlu menambahkan beberapa kelas ke kamus dari bentuk Map<String, Object> , maka ini bisa dilakukan seperti ini:


 void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, "com.smth.interceptor.AuditInterceptor"); } 

tetapi lebih baik menggunakan tipe eksplisit:


 import com.smth.interceptor.AuditInterceptor; void config(LocalContainerEntityManagerFactoryBean bean) { String property = "hibernate.session_factory.session_scoped_interceptor"; bean.getJpaPropertyMap().put(property, AuditInterceptor.class); } 

Dan ketika ada sesuatu seperti


 LocalContainerEntityManagerFactoryBean bean = builder .dataSource(dataSource) .packages( //...     ) .persistenceUnit("psql") .build(); 

Perlu dicatat bahwa metode packages() kelebihan beban dan menggunakan kelas:


Jangan memasukkan semua kacang dalam satu paket


Saya pikir dalam banyak proyek di Spring / Spring Booth Anda melihat struktur yang sama:


 root-package | \ repository/ entity/ service/ Application.java 

Di sini Application.java adalah kelas root dari aplikasi:


 @SpringBootApplication @EnableJpaRepositories(basePackageClasses = SomeRepository.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 

Ini adalah kode microservice klasik: komponen disusun dalam folder sesuai dengan tujuannya, kelas dengan pengaturannya adalah di root. Meskipun proyek ini kecil, maka semuanya baik-baik saja. Seiring pertumbuhan proyek, paket lemak muncul dengan puluhan repositori / layanan. Dan jika proyek tetap monolit, maka Gd bersama mereka. Tetapi jika tugas muncul untuk membagi aplikasi yang berantakan menjadi beberapa bagian, maka pertanyaan dimulai. Setelah mengalami rasa sakit ini sekali, saya memutuskan untuk mengambil pendekatan yang berbeda, yaitu mengelompokkan kelas berdasarkan domain mereka. Hasilnya adalah sesuatu seperti


 root-package/ | \ user/ | \ repository/ domain/ service/ controller/ UserConfig.java billing/ | \ repository/ domain/ service/ BillingConfig.java //... Application.java 

Di sini, paket user termasuk sub-paket dengan kelas-kelas yang bertanggung jawab atas logika pengguna:


 user/ | \ repository/ UserRepository.java domain/ UserEntity.java service/ UserService.java controller/ UserController.java UserConfig.java 

Sekarang di UserConfig Anda dapat menjelaskan semua pengaturan yang terkait dengan fungsi ini:


 @Configuration @ComponentScan(basePackageClasses = UserServiceImpl.class) @EnableJpaRepositories(basePackageClasses = UserRepository.class) class UserConfig { } 

Keuntungan dari pendekatan ini adalah, jika perlu, modul dapat lebih mudah dialokasikan untuk layanan / aplikasi terpisah. Ini juga berguna jika Anda bermaksud memodulasi proyek Anda dengan menambahkan module-info.java , menyembunyikan kelas utilitas dari dunia luar.


Itu saja, saya harap, pekerjaan saya bermanfaat bagi Anda. Jelaskan antipatterns Anda dalam komentar, kami akan mengatasinya bersama-sama :)

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


All Articles