Пролетта е може би една от най-популярните Java рамки, а също и мощен звяр, който да се опитоми. Въпреки че основните понятия са доста лесни за разбиране, за да станете силен разработчик на Spring, е необходимо известно време и усилия.
В тази статия ще разгледаме някои от най-често срещаните грешки през Spring, специално ориентирани към уеб приложения и Spring Boot. Като Уебсайт на Spring Boot заявява, Spring Boot отнема мнителен поглед за това как трябва да се изграждат готови за производство приложения, така че тази статия ще се опита да имитира този изглед и ще предостави преглед на някои съвети, които ще се включат добре в стандартното разработване на уеб приложения Spring Boot.
В случай, че не сте добре запознати с Spring Boot, но все пак искате да изпробвате някои от споменатите неща, аз съм ги създал хранилище на GitHub, придружаващо тази статия . Ако се почувствате изгубени по всяко време на статията, бих препоръчал да клонирате хранилището и да си поиграете с кода на вашата локална машина.
Удряме с тази често срещана грешка, защото „ не е измислено тук ”Синдромът е доста разпространен в света за разработка на софтуер. Симптомите, включително редовно пренаписване на парчета от често използван код и много разработчици изглежда страдат от него.
Въпреки че разбирането на вътрешността на дадена библиотека и нейното внедряване е в по-голямата си част добро и необходимо (и може да бъде и чудесен процес на обучение), вредно е за вашето развитие като софтуерен инженер да се справяте непрекъснато със същото изпълнение на ниско ниво подробности. Има причина, поради която съществуват абстракции и рамки като Spring, която е именно да ви отдели от повтарящата се ръчна работа и да ви позволи да се концентрирате върху детайли от по-високо ниво - обектите на вашия домейн и бизнес логиката.
Така че прегърнете абстракциите - следващия път, когато се сблъскате с определен проблем, направете първо бързо търсене и определете дали библиотеката, решаваща този проблем, вече е интегрирана в Spring; в днешно време има шанс да намерите подходящо съществуващо решение. Като пример за полезна библиотека ще използвам Проект Ломбок пояснения в примери за останалата част на тази статия. Lombok се използва като генератор на шаблонни кодове и мързеливият разработчик във вас, надявам се, не би трябвало да има проблем да се запознае с библиотеката. Като пример вижте какво „ стандартен Java боб ”Изглежда като при Ломбок:
@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }
Както можете да си представите, горният код се компилира в:
public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }
Обърнете внимание обаче, че най-вероятно ще трябва да инсталирате приставка, в случай че възнамерявате да използвате Lombok с вашата IDE. Може да се намери версията на приставката на IntelliJ IDEA тук .
Излагането на вашата вътрешна структура никога не е добра идея, защото създава гъвкавост в дизайна на услугата и следователно насърчава лоши практики за кодиране. „Изтичащите“ вътрешни елементи се проявяват, като правят структурата на базата данни достъпна от определени крайни точки на API. Като пример, да кажем, че следният POJO („Plain Old Java Object“) представлява таблица във вашата база данни:
@Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }
Да приемем, че съществува крайна точка, която трябва да има достъп до TopTalentEntity
данни. Колкото и изкушаващо да е да се върнете TopTalentEntity
случаи, по-гъвкаво решение би било създаването на нов клас, който да представлява TopTalentEntity
данни за крайната точка на API:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
По този начин, извършването на промени във фона на вашата база данни няма да изисква никакви допълнителни промени в сервизния слой. Помислете какво би се случило в случай на добавяне на поле „парола“ към TopTalentEntity
за съхраняване на хешове на пароли на вашите потребители в базата данни - без конектор като TopTalentData
, забравяйки да смените интерфейса на услугата, би случайно изложил някаква много нежелана тайна информация!
С нарастването на вашето приложение организацията на кодовете все повече започва да става все по-важен въпрос. По ирония на съдбата, повечето от добрите принципи на софтуерното инженерство започват да се разпадат в мащаб - особено в случаите, когато не се е обмисляло много върху дизайна на архитектурата на приложенията. Една от най-често срещаните грешки, които разработчиците впоследствие са склонни да се поддават, е смесването на проблемите с кода и е изключително лесно да се направи!
Това, което обикновено се счупва разделяне на опасенията е просто „изхвърляне“ на нова функционалност в съществуващи класове. Това, разбира се, е чудесно краткосрочно решение (за начало изисква по-малко писане), но неизбежно се превръща в проблем по-надолу по пътя, било то по време на тестване, поддръжка или някъде между тях. Помислете за следния контролер, който връща TopTalentData
от неговото хранилище:
@RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping('/toptal/get') public List getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
Отначало може да не изглежда, че има нещо особено нередно с тази част от кода; той предоставя списък на TopTalentData
който се извлича от TopTalentEntity
инстанции. Погледвайки отблизо обаче, можем да видим, че всъщност има няколко неща, които TopTalentController
се представя тук; а именно, това е картографиране на заявки към определена крайна точка, извличане на данни от хранилище и преобразуване на обекти, получени от TopTalentRepository
в различен формат. „По-чисто“ решение би било разделянето на тези опасения в техните собствени класове. Може да изглежда по следния начин:
@RestController @RequestMapping('/toptal') @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping('/get') public List getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
Допълнително предимство към тази йерархия е, че тя ни позволява да определим къде се намира функционалността само чрез проверка на името на класа. Освен това, по време на тестване можем лесно да заменим някой от класовете с фалшива реализация, ако възникне необходимост.
Темата за последователността не е непременно изключителна за Spring (или Java, в този смисъл), но все пак е важен аспект, който трябва да се има предвид при работата по пролетни проекти. Докато стилът на кодиране може да бъде дебат (и обикновено е въпрос на споразумение в екип или в цяла компания), наличието на общ стандарт се оказва голяма помощ за производителността. Това важи особено за многочленните екипи; последователността позволява предаването да се извърши, без да се изразходват много ресурси за държане на ръка или да се предоставят дълги обяснения относно отговорностите на различните класове
Помислете за пролетен проект с различни конфигурационни файлове, услуги и контролери. Постоянната семантична последователност при именуването им създава лесно търсима структура, където всеки нов разработчик може да се справя с кода; добавяне на суфикси на Config към вашите конфигурационни класове, суфикси на услуги към вашите услуги и суфикси на Controller към вашите контролери, например.
Тясно свързано с темата за последователността, обработката на грешки от страна на сървъра заслужава конкретен акцент. Ако някога се е налагало да обработвате отговори на изключения от лошо написан API, вероятно знаете защо - може да е мъчно да анализирате правилно изключенията и още по-болезнено да определите причината, поради която тези изключения са възникнали на първо място.
Като разработчик на API, в идеалния случай бихте искали да покриете всички крайни точки, насочени към потребителя, и да ги преведете в често срещан формат за грешка. Това обикновено означава да имате общ код за грешка и описание, а не решението за отстраняване на а) връщане на съобщение „500 Вътрешна грешка в сървъра“, или б) просто изхвърляне на проследяването на стека на потребителя (което всъщност трябва да се избягва на всяка цена тъй като тя излага вашите вътрешни елементи, освен че е трудна за работа с клиента).
Пример за често срещан формат за отговор на грешка може да бъде:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
Нещо подобно на това се среща често в повечето популярни API и има тенденция да работи добре, тъй като може лесно и систематично да се документира. Превеждането на изключения в този формат може да се извърши чрез предоставяне на @ExceptionHandler
анотация към метод (пример за анотация е в Common Mistake # 6).
Независимо дали се среща в десктоп или уеб приложения, Spring или no Spring, многопоточността може да бъде трудна ядка. Проблемите, причинени от паралелно изпълнение на програми, са нервно неуловими и често пъти са изключително трудни за отстраняване на грешки - всъщност поради естеството на проблема, след като разберете, че имате работа с проблем с паралелно изпълнение, който вероятно ще трябва да се откажете изцяло от дебъгера и да проверите кода си „на ръка“, докато откриете основната причина за грешка. За съжаление, решение за изрязване на бисквитки не съществува за решаване на такива проблеми; в зависимост от конкретния ви случай ще трябва да оцените ситуацията и след това да атакувате проблема от ъгъла, който смятате за най-добър.
В идеалния случай, разбира се, бихте искали изобщо да избягвате многопоточни грешки. Отново не съществува универсален подход за това, но ето някои практически съображения за отстраняване на грешки и предотвратяване на грешки при многопоточност:
Първо, винаги помнете въпроса за „глобалната държава“. Ако създавате многонишко приложение, абсолютно всичко, което може да се модифицира в световен мащаб, трябва да бъде внимателно наблюдавано и, ако е възможно, изобщо да бъде премахнато. Ако има причина, поради която глобалната променлива трябва да остане модифицируема, внимателно използвайте синхронизация и проследете ефективността на приложението си, за да потвърдите, че не е бавно поради нововъведените периоди на изчакване.
Това идва направо от функционално програмиране и, адаптиран към OOP, заявява, че трябва да се избягват изменчивостта на класа и променящото се състояние. Накратко, това означава предходни методи за задаване и разполагане с частни крайни полета във всичките ви класове модели. Единственият път, когато техните стойности се мутират, е по време на строителството. По този начин можете да сте сигурни, че няма да възникнат проблеми със споровете и че достъпът до свойствата на обекта ще предоставя правилните стойности през цялото време.
Преценете къде вашето приложение може да причини проблеми и превантивно регистрирайте всички важни данни. Ако възникне грешка, ще бъдете благодарни да имате информация, посочваща кои заявки са получени, и да имате по-добра представа защо вашето приложение се е държало неправилно. Отново е необходимо да се отбележи, че регистрирането въвежда допълнителни входни / изходни файлове и следователно не трябва да се злоупотребява, тъй като това може сериозно да повлияе на ефективността на приложението ви.
Винаги, когато имате нужда от създаване на собствени нишки (например за отправяне на асинхронни заявки към различни услуги), използвайте повторно съществуващите безопасни внедрения, вместо да създавате свои собствени решения. Това в по-голямата си част ще означава използване ExecutorServices и изчистения функционален стил на Java 8 CompletableFutures за създаване на нишка. Spring също позволява асинхронна обработка на заявки чрез Отложен резултат клас.
Нека си представим, че нашата услуга TopTalent от по-рано изисква крайна точка за добавяне на нови Топ таланти. Освен това, да кажем, че по някаква наистина основателна причина всяко ново име трябва да е с дължина точно 10 знака. Един от начините да направите това може да бъде следният:
@RequestMapping('/put') public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); }
Горното обаче (освен че е лошо изградено) всъщност не е „чисто“ решение. Проверяваме за повече от един тип валидност (а именно, че TopTalentData
не е нула, и че TopTalentData.name
не е нула, и че TopTalentData.name
е дълъг 10 символа), както и да се изведе изключение, ако данните са невалидни.
Това може да бъде изпълнено много по-чисто, като се използва Хибриден валидатор с пролетта. Нека първо рефакторираме addTopTalent
метод в подкрепа на валидирането:
@RequestMapping('/put') public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }
Освен това ще трябва да посочим какво свойство искаме да потвърдим в TopTalentData
клас:
public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }
Сега Spring ще прихване заявката и ще я потвърди, преди методът да бъде извикан - няма нужда да се използват допълнителни ръчни тестове.
Друг начин, по който бихме могли да постигнем същото е чрез създаване на собствени анотации. Въпреки че обикновено използвате персонализирани анотации само когато вашите нужди надхвърлят Вграденият набор от ограничения на Hibernate , за този пример нека се преструваме, че @Length не съществува. Бихте направили валидатор, който проверява за дължина на низа, като създадете два допълнителни класа, един за валидиране и друг за анотиране на свойства:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default 'String length does not match expected'; Class[] groups() default {}; Class[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) }
Имайте предвид, че в тези случаи най-добрите практики за разделяне на опасенията изискват от вас да маркирате свойство като валидно, ако е нула (s == null
в метода isValid
) и след това да използвате @NotNull
анотация, ако това е допълнително изискване за имота:
public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }
Докато XML беше необходимост за предишните версии на Spring, в днешно време по-голямата част от конфигурацията може да се направи изключително чрез Java код / анотации; Конфигурациите на XML просто се представят като допълнителен и ненужен код.
Тази статия (както и придружаващото я хранилище GitHub) използва анотации за конфигуриране на Spring и Spring знае кои зърна трябва да свърже, тъй като основният пакет е анотиран с @SpringBootApplication
съставна анотация, така:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Комбинираната анотация (можете да научите повече за нея в Пролетна документация просто дава подсказка на Spring за това кои пакети трябва да се сканират, за да се извлекат зърна. В конкретния ни случай това означава, че следното под горния (co.kukurin) пакет ще се използва за окабеляване:
@Component
(TopTalentConverter
, MyAnnotationValidator
)@RestController
(TopTalentController
)@Repository
(TopTalentRepository
)@Service
(TopTalentService
) класовеАко имахме допълнителни @Configuration
анотирани класове, те също ще бъдат проверени за Java-базирана конфигурация.
Проблем, който често се среща при разработката на сървъри, е разграничаването между различни типове конфигурации, обикновено вашите производствени и развойни конфигурации. Вместо да замествате ръчно различни записи в конфигурацията всеки път, когато превключвате от тестване към внедряване на приложението си, по-ефективен начин би бил използването на профили.
Помислете за случая, когато използвате база данни в паметта за локално развитие, с MySQL база данни в производство. По същество това би означавало, че ще използвате различен URL и (надявам се) различни идентификационни данни за достъп до всеки от двата. Нека да видим как това може да се направи два различни конфигурационни файла:
# set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:
spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2
Предполага се, че не бихте искали случайно да извършвате каквито и да е действия върху вашата производствена база данни, докато се занимавате с кода, така че има смисъл да зададете профила по подразбиране на dev. След това на сървъра можете ръчно да замените конфигурационния профил, като предоставите -Dspring.profiles.active=prod
параметър към JVM. Като алтернатива можете също да зададете променливата на средата на вашата операционна система на желания профил по подразбиране.
Правилното използване на инжектиране на зависимост с Spring означава да му позволите да свърже всички ваши обекти заедно, като сканира всички желани класове конфигурация; това се оказва полезно за разединяване на връзките и също така улеснява много тестването. Вместо тесни класове за свързване, като направите нещо подобно:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }
Разрешаваме на Spring да извърши окабеляването вместо нас:
public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }
Беседа на Google на Misko Hevery обяснява „защо“ в дълбочина инжектирането на зависимост, така че нека вместо това да видим как се използва на практика. В раздела за разделяне на проблеми (Общи грешки # 3) създадохме клас на услуга и контролер. Да приемем, че искаме да тестваме контролера при предположението, че TopTalentService
се държи коректно. Можем да вмъкнем фиктивен обект вместо действителното изпълнение на услугата, като предоставим отделен конфигурационен клас:
@Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of('Mary', 'Joel').map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } }
След това можем да инжектираме фиктивния обект, като кажем на Spring да използва SampleUnitTestConfig
като доставчик на конфигурация:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })
Тогава това ни позволява да използваме контекстна конфигурация, за да инжектираме персонализирания боб в единичен тест.
Въпреки че идеята за модулно тестване ни е отдавна, много разработчици изглежда или „забравят“ да направят това (особено ако не е задължително ), или просто го добавете като допълнителна мисъл. Това очевидно не е желателно, тъй като тестовете трябва не само да проверят верността на вашия код, но и да служат като документация за това как приложението трябва да се държи в различни ситуации.
Когато тествате уеб услуги, рядко правите изключително „чисти“ модулни тестове, тъй като комуникацията през HTTP обикновено изисква да извикате Spring’s DispatcherServlet
и вижте какво се случва, когато действително HttpServletRequest
е получено (което го прави интеграция тест, занимаващ се с валидиране, сериализация и др.). Бъдете спокойни , Java DSL за лесно тестване на REST услуги, на върха на MockMVC, се оказа много елегантно решение. Обмислете следния кодов фрагмент с инжектиране на зависимост:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get('/toptal/get'); // then response.then().statusCode(200); response.then().body('name', hasItems('Mary', 'Joel')); } }
SampleUnitTestConfig
свързва фалшиво изпълнение на TopTalentService
в TopTalentController
докато всички останали класове са свързани с помощта на стандартната конфигурация, изведена от сканиращите пакети, вкоренени в пакета за клас на приложение. RestAssuredMockMvc
се използва просто за създаване на лека среда и изпращане на GET
искане до /toptal/get
крайна точка.
Пролетта е мощна рамка, с която е лесно да започнете, но изисква известна отдаденост и време, за да постигнете пълно майсторство. Отделянето на време за запознаване с рамката определено ще подобри вашата производителност в дългосрочен план и в крайна сметка ще ви помогне да напишете по-чист код и да станете по-добър разработчик.
Ако търсите допълнителни ресурси, Пролет в действие е добра практическа книга, обхващаща много основни пролетни теми.