Spring boot

Contrôleurs

TODO: RestController, ResponseBody

Bases

Une classe contrôleur est déclarée à l'aide d'un @Controller.

@Controller
public class MyController {}

Pour définir un handler sur route, il faut créer une fonction, et utiliser un attribut RequestMapping.

@RequestMapping prend en premier paramètre la route sur laquelle se bind (@RequestMapping("/")), et la méthode est définissable à l'aide du paramètre nommé method, par exemple method = RequestMethod.GET.

@Controller
public class MyController {
    @RequestMapping("/", method = RequestMethod.GET)
    public void index() {
        // ...
    }
}

Additionnellement, Spring fournit des raccourcis, nommés @<méthode>Mapping, par exemple @GetMapping.

Préfixe de chemin

Dans le cas où toutes les méthodes de notre contrôleur ont un préfixe commun, par exemple /books/*, on peut utiliser @RequestMapping sur la classe.

@Controller
@RequestMapping("/books")
public class BookController {
    @GetMapping("/")
    public String listBooks() {
        // ...
    }

    @GetMapping("/by-author")
    public String listBooksGroupedByAuthor() {
        // ...
    }
}

Sur cet exemple, les routes crées sont /books et /books/by-author.

Réponse JSON

Un contrôleur @RestController permet de renvoyer des objets bruts, et de laisser Spring s'occuper de la sérialisation en JSON.

@RestController
public class TestController {
    @GetMapping("/")
    public TestResponse handle() {
        // Doing stuff, returning an instance
    }

    private static class TestResponse {
        private final String field;
        private final Integer anotherField;

        TestResponse(String field, Integer anotherField) {
            this.field = field;
            this.anotherField = anotherField;
        }

        public String getField() { return field; }

        public Integer getAnotherField() { return anotherField; }
    }
}
{
    "field": "value",
    "anotherField": 666
}

Il est possible de renommer la clé qui sera utilisée pour le champ dans la réponse, à l'aide de l'annotation @JsonProperty appliquable sur le getter.

@JsonProperty("myOwnCustomField")
public String getField() { return field; }
{
    "myOwnCustomField": "value",
    "anotherField": 666
}

Cycle de vie d'un bean

Cycle de création

Spring dispose de bien plus d'interfaces de type "Aware".

Elles sont toutes listées sur cette référence.

Cycle de destruction

Annotation de fonction @Bean

L'annotation @Bean prend plusieurs valeurs optionnelles de configuration.

Lien de la référence sur @Bean.

Injection de dépendance

Sélection statique d'implémentation

Dans le cas où un service (interface) a plusieurs implémentations, pour spécifier quelle implémentation est attendue, l'annotation @Qualifier est utilisée.

public interface MockInterface {
    public String doSomething();
}
@Service
public class MockImpl implements MockInterface {
    public String doSomething() { return "Meow"; }
}
@Service
public class SecondMockImpl implements MockInterface {
    public String doSomething() { return "Another Meow"; }
}

Avec le contrôleur suivant, Spring plantera car il n'est pas capable de déterminer quelle implémentation de MockInterface utiliser.

@Controller
public class HelloController {
    private final MockInterface service;

    public HelloController(MockInterface service) {
        this.service = service;
    }
}

L'attribut @Qualifier("beanName") peut être utilisé pour "indiquer" à Spring quelle implémentation utiliser, où beanName est, par défaut, le nom de la classe en question, avec la première lettre en minuscule.

@Controller
public class HelloController {
    private final MockInterface service;

    public HelloController(@Qualifier("secondMockImpl") MockInterface service) {
        this.service = service; // instanceof SecondMockImpl
    }
}

De façon simplifiée, si, parmi toutes ces implémentations, une seule est voulue dans le cas "général" (par défaut), il suffit de lui appliquer l'annotation @Primary, qui indique, dans un cas où seule une implémentation dispose de cet attribut, qu'il s'agira de la classe dont Spring doit se servir.

// ...
@Primary
@Service
public class SecondMockImpl implements MockInterface {
    public String doSomething() { return "Another Meow"; }
}
@Controller
public class HelloController {
    private final MockInterface service;

    public HelloController(MockInterface service) {
        this.service = service; // instanceof SecondMockImpl
    }
}

Il est possible, dans un cas où @Primary n'est pas utilisé, d'automatiquement obtenir instance du bon type en nommant la variable avec la valeur voulue du beanName (ex. private final MockInterface secondMockImpl).

Ça a pour désavantage d'inclure un comportement implicite, et la possibilité de bug dans le cas où @Primary serait utilisé, car @Primary prend la priorité sur le nom de la variable.

A éviter au maximum, donc.

Sélection dynamique d'implémentation

Pour dynamiquement sélectionner une implémentation, on peut utiliser le principe de "profil".

Chaque bean d'implémentation est marqué d'un @Profile("key"), où key est la clé du profil voulu.

Ce bean ne sera que utilisé lorsque le profil key est utilisé.

Contrairement aux qualificateurs (statiques, nécessitant de recompiler le code), on peut décider de changer de profil durant le fonctionnement de notre app, nous permettant de remplacer les implémentations utilisées à l'aide de configuration (par exemple, pour changer des types data sources pour des tests d'intégration).

La différence est que, au lieu de marquer, au niveau de la variable cible, le type attendu, on déclare, au niveau de l'implémentation, le mode visé.

La sélection statique et la sélection dynamique sont deux outils différents avec des use-case différents.

Configuration

Pour configurer le profil utilisé, on utilise la clé spring.profiles.active.

Par exemple,

interface IMock {
    void push(String data);
}
@Service
@Profile("test")
class ActionMQImpl implements IMock {
    void push(String data) {
        // Pushing to ActionMQ
    }
}
@Service
@Profile("prod")
class RabbitMQImpl implements IMock {
    void push(String data) {
        // Pushing to RabbitMQ
    }
}

La configuration de production aurait donc la clé suivante dans le fichier application.properties: spring.profiles.active=prod.

Pour déclarer qu'une implémentation ne doit pas être utilisée lorsqu'un profil est utilisé, il suffit de préfixer le profil d'un !, par exemple !prod permettra d'utiliser l'implémentation à condition que le profil actif ne soit pas prod.

Bean par défaut

Dans le cas où aucun profil n'a été sélectionné lors de la configuration de spring, tout service déclaré sans profil explicite, ou avec la clé default sera sélectionné.

Si un bean n'est pas explicitement déclaré comme associé à un profil, il est automatiquement associé au profil default.

Si la configuration déclare l'utilisation d'un profil, et que aucune implémentation ne supporte ce profil, Spring ne prendra pas default!

Il plantera simplement en disant ne pas être capable de deviner le profil à utiliser.

Si on veut à la fois associer une clé à une implémentation, et la déclarer comme défaut, on peut passer un tableau à @Profile.

@Service
@Profile({"prod", "default"})
class RabbitMQImpl implements IMock {
    void push(String data) {
        // Pushing to RabbitMQ
    }
}

Profil par défaut

La propriété spring.profiles.default permet de définir le profil à utiliser par défaut (quand aucun profil n'est renseigné).

Liste d'implémentations

Il est possible d'obtenir une liste de toutes les implémentations d'une interface, en changeant le type de paramètre (ou d'attribut) en une liste.

Par exemple, IMock devient List<IMock>.

Source d'instance

Une classe marquée par l'annotation @Component, ou par toute autre annotation l'étendant (@Service, etc.), peut être source d'instance, comme vu précédemment.

Additionnellement, il est possible de fournir une instance grâce au retour d'une fonction.

Par exemple, dans le cas où on veut obtenir une instance d'un objet disposant d'un paramètre de type générique, on peut construire cette instance à l'aide de fonctions.

public interface IMock<T extends List> {
    // ...
}
@Configuration // Annotation nécessaire pour que les fonctions annotée de `@Bean` soient visibles
public class MockFactory {
    @Bean
    public IMock<ArrayList> buildArrayListInstance() {
        // ...
    }

    @Bean
    public IMock<LinkedList> buildLinkedListInstance() {
        // ...
    }
}

Si une autre classe déclare avoir besoin d'une instance utilisant ArrayList, buildArrayListInstance sera appelé et son résultat sera fourni à la classe.

@Component
public class MyComponent {
    public MyComponent(IMock<ArrayList> arrayInstance) {
        // ...
    }
}

Note: L'injection ici suit un principe de localisation de service: "Si j'ai une instance en cache, je la fournis, sinon je recherche et j'obtiens une instance".

Modèles

Bases

Un modèle est un POJO à qui on met l'attribut @Entity.

@Entity
public class MyEntity {
    // ...
}

Pour définir un ID clé-primaire, on utilise @Id. Pour l'auto-incrémenter, on utilise @GeneratedValue(strategy = GenerationType.AUTO)

@Entity
public class MyEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}

On a besoin d'un constructeur vide explicite, et de getters/setters pour chaque champ, ainsi que de la méthode equals et hashCode basée sur id.

Relations

One to One

Mettons une classe Paper et une classe Author.

Un Paper a un Author, et on ignore tous les autres use cases ici (Author ayant plusieurs Paper, par exemple).

On a donc Paper(#author_id => Author(id)).

Pour composer une telle relation, on utilise @OneToOne sur l'entité propriétaire. Ici, Paper est "propriétaire" de Author.

@Entity
public class Author {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;

    public Author() {}
    public Author(String name) { this.name = name; }
    // getters, setters, etc.
}
@Entity
public class Paper {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    // ...

    public Paper() {}
    public Paper(String title) { this.title = title; }
    // getters, setters, etc.

    // OneToOne relationship where Paper owns Author
    @OneToOne
    private Author author;
}

Il n'y a rien d'autre à effectuer pour déclarer une 1-1 de base.

Configuration de beans et properties

En cours de découverte, sujet à corrections et améliorations.

Pour configurer des beans custom, on peut utiliser les fichiers properties, en faisant une @Configuration class dédiée.

Structure de base

La base consiste en quelques étapes:

Il faudra noter que la valeur définie dans @Value doit être définie, sinon Spring boot plantera au démarrage.

Exemple d'une classe prenant des informations de connexion, et du fichier properties de base

@Configuration
public class AuthConfig {
    @Value("${auth.username}")
    String username;

    @Value("${auth.password"})
    String password;

    @Bean
    public Auth auth() {
        return new Auth(username, password);
    }
}
username = Artemis
password = Cats4Life

Environnement

Une valeur peut être directement fournie par l'environnement système.

Pour qu'elle soit reconnue, on suivra les conventions suivantes:

Fichiers properties custom

Il est possible de charger les valeurs d'un ou de plusieurs fichiers properties, en se servant de l'annotation @PropertySource.

Cette annotation prend un classpath, ou une liste de classpaths, sous le format texte classpath:<value>.

Par exemple, @PropertySource("classpath:config/auth.properties") va charger les valeurs du fichier présent dans /src/main/resources/config/auth.properties.

Profils

Il est possible de créer des fichiers properties de sorte à ce qu'ils soient uniquement utilisés lorsqu'un certain profil est chargé.

Pour ce faire, le fichier doit s'appeler <nom>-<profil>.properties.

Par exemple, application-test.properties pour le profil test.

A noter que le contenu du fichier spécifique à un profil sera pris en compte avant le contenu du fichier "général".

Documentation officielle ici.

YAML

Le format YAML est utilisable pour configurer Spring Boot.

Il suffit de renommer le fichier *.properties en *.yaml (ou *.yml).

Dans le cas d'un fichier custom (pointé par @PropertySource), il faut aussi corriger la source, ex. @PropertySource("classpath:config/auth.yml").

Vues

Bases

Pour joindre des données à une vue, on se sert de la classe org.springframework.ui.Model. On en récupère une instance par injection de dépendance sur notre fonction de handler.

@Controller
public class MyController {
    @GetMapping("/")
    public void index(Model model) {
        // ...
    }
}

Ce modèle va nous permettre d'ajouter des données qu'on voudra par la suite afficher sur notre vue.

Additionnellement, pour que Spring sache quelle vue utiliser, il faut renvoyer, en résultat de notre méthode, son nom (le nom de index.html étant index).

@Controller
public class MyController {
    @GetMapping("/")
    public String index(Model model) {
        // ...
        return "index";
    }
}

Spring va alors chercher à render la template disponible au chemin /templates/index.html. Si le projet est géré par gradle ou maven, ce dossier templates doit être présent dans /main/resources/.

Résumé rapide de Thymeleaf

Namespace

Pour correctement composer un fichier HTML utilisant le format thymeleaf, il faut inclure le namespace th.

L'inclusion s'effectue au niveau du tag ouvrant <html>.

<!doctype HTML>
<html xmlns:th="http://www.thymeleaf.org/">
<!-- ... -->
</html>

Résumé des tags

NOTE: Toute variable passée au modèle est accessible suivant la syntaxe ${key}, où key est la clé définie en premier paramètre de la fonction Model::addParameter.