Bonjour à tous. Aujourd'hui, nous partageons la première partie de l'article, dont la traduction a été préparée spécifiquement pour les étudiants du cours "Developer on the Spring Framework" . Commençons!
Spring est peut-être l'une des plateformes de développement Java les plus populaires. Il s'agit d'un outil puissant, mais plutôt difficile à apprendre. Ses concepts de base sont assez faciles à comprendre et à apprendre, mais il faut du temps et des efforts pour devenir un développeur Spring expérimenté.
Dans cet article, nous examinerons certaines des erreurs les plus courantes commises lors de l'utilisation de Spring et liées, en particulier, au développement d'applications Web et à l'utilisation de la plate-forme Spring Boot. Comme indiqué sur
le site Web de Spring Boot , Spring Boot adopte une approche
standardisée pour créer des applications prêtes à l'emploi, et cet article suivra cette approche. Il fournira un certain nombre de recommandations qui peuvent être utilisées efficacement dans le développement d'applications Web standard basées sur Spring Boot.
Dans le cas où vous n'êtes pas très familier avec la plate-forme Spring Boot, mais que vous souhaitez expérimenter avec les exemples de cet article, j'ai créé un
référentiel GitHub avec des matériaux supplémentaires pour cet article . Si à un moment donné vous êtes un peu confus en lisant cet article, je vous conseille de créer un clone de ce référentiel et d'expérimenter le code sur votre ordinateur.
Erreur courante n ° 1: aller trop bas dans la programmation
Nous succombons facilement à cette erreur répandue, car le
«syndrome de rejet du développement de quelqu'un d'autre» est assez typique de l'environnement de programmation. L'un de ses symptômes est la réécriture constante de morceaux de code couramment utilisés, et c'est un symptôme vu par de nombreux programmeurs.
L'étude de la structure interne et des fonctionnalités d'implémentation d'une bibliothèque particulière est souvent un processus utile, nécessaire et intéressant, mais si vous écrivez constamment le même type de code tout en effectuant une implémentation de bas niveau des fonctions, cela peut s'avérer néfaste pour vous en tant que développeur de logiciels. C'est pourquoi des abstractions et des plates-formes telles que Spring existent et sont utilisées - elles vous épargnent du travail manuel répétitif et vous permettent de vous concentrer sur les objets de votre domaine et la logique du code de programme à un niveau supérieur.
Par conséquent, les abstractions ne doivent pas être évitées. La prochaine fois que vous serez confronté à la nécessité de résoudre un problème spécifique, effectuez d'abord une recherche rapide et découvrez si la bibliothèque qui résout ce problème est déjà intégrée à Spring - vous trouverez probablement une solution clé en main appropriée. Une telle bibliothèque utile est
Project Lombok , les annotations à partir desquelles je vais utiliser dans les exemples de cet article. Lombok est utilisé comme générateur de code de modèle, donc le développeur paresseux qui vit en chacun de nous sera très heureux de se familiariser avec cette bibliothèque. Voir, par exemple, à quoi ressemble le
composant JavaBean standard dans Lombok:
@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }
Comme vous l'avez peut-être déjà compris, le code ci-dessus est converti au format suivant:
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() { } }
Notez que vous devrez probablement installer le plug-in approprié si vous souhaitez utiliser Lombok dans votre environnement de développement intégré. La version de ce plug-in pour IntelliJ IDEA est disponible
ici .
Erreur courante numéro 2. «Fuite» des structures internes
Donner accès aux structures internes n'a jamais été une bonne idée, car il nuit à la flexibilité du modèle de service et, par conséquent, contribue à la formation d'un mauvais style de programmation. La «fuite» des structures internes se manifeste dans le fait que la structure de la base de données devient accessible à partir de certains points de terminaison API. Par exemple, supposons que le «bon ancien objet Java» (POJO) suivant représente une table dans votre base de données:
@Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }
Supposons qu'il existe un point de terminaison qui doit accéder aux données d'un objet
TopTalentEntity
.
TopTalentEntity
des instances de
TopTalentEntity
semble être une idée tentante, mais une solution plus flexible serait de créer une nouvelle classe qui représente les données TopTalentEntity pour le point de terminaison API:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
Ainsi, apporter des modifications aux composants internes de la base de données ne nécessitera pas de modifications supplémentaires au niveau du service. Voyons ce qui se passe si le champ de mot de passe est ajouté à la classe
TopTalentEntity
pour stocker les hachages de mot de passe utilisateur dans la base de données: s'il n'y a pas de connecteur, tel que
TopTalentData
, et que le développeur oublie de changer la partie interface du service, cela peut entraîner une divulgation très indésirable d'informations secrètes!
Erreur courante numéro 3. Combiner des fonctions qu'il serait préférable de distribuer dans le code
L'organisation du code d'application à mesure qu'il grandit devient une tâche de plus en plus importante. Curieusement, la plupart des principes de programmation efficace cessent de fonctionner quand une certaine échelle de développement est atteinte, surtout si l'architecture de l'application n'a pas été bien pensée. Et l'une des erreurs les plus fréquemment commises est la combinaison de fonctions dans le code qui sont plus raisonnables à implémenter séparément.
La raison de la violation du principe de
séparation des responsabilités est généralement l'ajout de nouvelles fonctionnalités aux classes existantes. C'est peut-être une bonne solution momentanée (en particulier, elle nécessite d'écrire une plus petite quantité de code), mais à l'avenir, cela deviendra inévitablement un problème, y compris aux étapes de test et de maintenance du code et entre eux. Considérez le contrôleur suivant qui renvoie
TopTalentData
partir du référentiel:
@RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
À première vue, il semble qu'il n'y ait aucune erreur évidente dans ce fragment de code. Il fournit une liste d'objets
TopTalentData
, qui est extraite des instances de la classe
TopTalentEntity
. Mais si vous regardez le code de plus près, nous verrons qu'en réalité
TopTalentController
effectue plusieurs actions ici, à savoir, il corrèle les demandes avec un point de terminaison spécifique, récupère les données du référentiel et convertit les objets reçus de
TopTalentRepository
dans un format différent. Une solution plus propre devrait séparer ces fonctions en classes distinctes. Par exemple, cela pourrait ressembler à ceci:
@RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> 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()); } }
Un avantage supplémentaire de cette hiérarchie est qu'elle nous permet de comprendre où se trouve une fonction simplement en regardant le nom de la classe. Par la suite, pendant les tests, nous pouvons facilement remplacer le code de l'une de ces classes par du code de substitution, si le besoin s'en fait sentir.
Numéro d'erreur commun 4. Code uniforme et mauvaise gestion des erreurs
Le sujet de l'uniformité du code n'est pas propre à Spring (ou à Java en général), mais est néanmoins un aspect important qui doit être pris en compte lors de l'utilisation de projets dans Spring. Le choix d'un style de programmation spécifique peut être un sujet de discussion (et est généralement cohérent au sein de l'équipe de développement ou dans toute l'entreprise), mais dans tous les cas, la présence d'une norme commune pour l'écriture de code contribue à augmenter l'efficacité du travail. Cela est particulièrement vrai si plusieurs personnes travaillent sur le code. Le code uniforme peut être transmis d'un développeur à l'autre sans dépenser beaucoup d'efforts pour le maintenir ou pour de longues explications sur la raison pour laquelle ces classes ou ces classes sont nécessaires.
Imaginez qu'il existe un projet Spring dans lequel il existe divers fichiers de configuration, services et contrôleurs. Suite à l'uniformité sémantique de leur dénomination, nous créons une structure par laquelle vous pouvez facilement rechercher et dans laquelle tout développeur peut facilement comprendre le code. Par exemple, vous pouvez ajouter le suffixe Config aux noms des classes de configuration, le suffixe Service aux noms de service et le suffixe Controller aux noms de contrôleur.
La gestion des erreurs côté serveur est étroitement liée à l'uniformité du code et mérite une attention particulière. Si vous avez déjà géré des exceptions provenant d'une API mal écrite, vous savez probablement à quel point il est difficile de comprendre correctement la signification de ces exceptions, et encore plus difficile de déterminer pourquoi elles se sont réellement produites.
En tant que développeur d'API, vous souhaitez idéalement couvrir tous les points finaux avec lesquels l'utilisateur travaille et les amener à utiliser un seul format de message d'erreur. Cela signifie généralement que vous devez utiliser les codes d'erreur standard et leur description et abandonner les décisions douteuses telles que donner à l'utilisateur un message "500 Internal Server Error" ou les résultats d'une trace de pile (cette dernière option, soit dit en passant, doit être évitée par tous les moyens, car vous révélez des données internes, et de plus, de tels résultats sont difficiles à traiter du côté client).
Voici, par exemple, à quoi pourrait ressembler le format général du message d'erreur:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
Un format similaire à celui-ci se trouve souvent dans les API les plus populaires et, en règle générale, fonctionne très bien car il peut être facilement et systématiquement documenté. Vous pouvez convertir une exception dans ce format en ajoutant l'annotation
@ExceptionHandler
à la méthode (pour un exemple d'annotation, consultez la section "Erreur commune # 6").
Erreur courante numéro 5. Travail incorrect avec le multithreading
L'implémentation du multithreading peut être une tâche difficile, qu'elle soit utilisée dans des applications de bureau ou dans des applications Web, dans des projets Spring ou des projets pour d'autres plates-formes. Les problèmes causés par l'exécution parallèle de programmes sont difficiles à suivre, et les traiter avec un débogueur est souvent très difficile. Si vous comprenez que vous avez affaire à une erreur liée à l'exécution parallèle, vous devrez très probablement abandonner le débogueur et examiner le code manuellement jusqu'à ce que la cause première de l'erreur soit trouvée. Malheureusement, il n'existe aucun moyen standard de résoudre ces problèmes. Dans chaque cas, il faut évaluer la situation et «attaquer» le problème avec la méthode qui semble la meilleure dans les conditions données.
Idéalement, bien sûr, vous souhaitez éviter complètement les erreurs associées au multithreading. Et bien qu'il n'y ait pas de recette universelle ici, je peux toujours offrir quelques recommandations pratiques.
Évitez d'utiliser des états globaux
Tout d'abord, souvenez-vous toujours du problème des «États mondiaux». Si vous développez une application multithread, vous devez surveiller attentivement absolument toutes les variables modifiables globalement et, si possible, vous en débarrasser complètement. Si vous avez encore une raison pour laquelle la variable globale doit être modifiable,
synchronisez et surveillez correctement les performances de votre application - vous devez vous assurer qu'elle ne ralentit pas en raison de périodes d'attente supplémentaires.
Évitez les objets mutables
Cette idée vient directement des principes de
programmation fonctionnelle et, étant adaptée aux principes de la POO, stipule que les classes mutables et les états mutables doivent être évités. En bref, cela signifie que vous devez vous abstenir de définir des méthodes (setters) et avoir des champs privés avec le modificateur final dans toutes les classes de modèle. La seule fois où leurs valeurs changent, c'est lorsque l'objet est créé. Dans ce cas, vous pouvez être sûr qu'aucun problème n'est associé à la compétition pour les ressources, et lors de l'accès aux propriétés de l'objet, les valeurs correctes seront toujours obtenues.
Consigner les données importantes
Évaluez les endroits où des problèmes peuvent survenir dans l'application et configurez à l'avance une journalisation de toutes les données importantes. Si une erreur se produit, vous serez heureux d'avoir des informations sur les demandes reçues et vous serez en mesure de mieux comprendre les raisons du dysfonctionnement de votre application. Cependant, gardez à l'esprit que la journalisation implique des E / S de fichiers supplémentaires et ne doit pas être utilisée de manière abusive, car cela peut nuire aux performances de l'application.
Utilisez des implémentations de threads prêtes à l'emploi
Lorsque vous devez générer vos threads (par exemple, pour créer des demandes asynchrones vers divers services), utilisez des implémentations de threads sûres prêtes à l'emploi au lieu de créer les vôtres. Dans la plupart des cas, vous pouvez utiliser les abstractions
ExecutorService et les spectaculaires abstractions fonctionnelles
CompletableFuture pour Java 8. La plate-forme Spring vous permet également de gérer les demandes asynchrones à l'aide de la classe
DeferredResult .
La fin de la première partie.
Lisez la deuxième partie.