web-dev-qa-db-fra.com

Code propre - Où appliquer @Autowired?

Je vais commencer par un exemple simple. Vous disposez d'une application de démarrage Spring qui exécute une classe CommandLineRunner lors de l'initialisation.

// MyCommandLineRunner.Java
public class MyCommandLineRunner implements CommandLineRunner {
    private final Log logger = LogFactory.getLog(getClass());
    @Autowired //IntelliJ Warning
    private DataSource ds;
    @Override
    public void run(String... args) throws Exception {
        logger.info("DataSource: " + ds.toString());
    }
}
// Application.Java
@SpringBootApplication
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args); 
    }
    @Bean
    public MyCommandLineRunner schedulerRunner() {
        return new MyCommandLineRunner();
    }
}

Maintenant, comme ça, ça marche, tout va bien. Cependant, IntelliJ signale un avertissement où @Autowired est localisé (j'ai marqué où dans le commentaire)

L'équipe Spring recommande: Utilisez toujours l'injection de dépendance basée sur le constructeur dans vos beans. Utilisez toujours des assertions pour les dépendances obligatoires.

Maintenant, si je suis ceci, j'ai une injection de dépendance basée sur le constructeur

@Autowired
public MyCommandLineRunner(DataSource ds) { ... }

Cela signifie également que je dois modifier Application.Java également, car le constructeur a besoin d'un argument. Dans Application.Java si j'essaie d'utiliser l'injection de setter, j'obtiendrai le même avertissement. Si je refactorise cela aussi, je vais me retrouver avec, à mon avis, du code désagréable.

// MyCommandLineRunner.Java
public class MyCommandLineRunner implements CommandLineRunner {
    private final Log logger = LogFactory.getLog(getClass());
    private DataSource ds;
    @Autowired // Note that this line is practically useless now, since we're getting this value as a parameter from Application.Java anyway.
    public MyCommandLineRunner(DataSource ds) { this.ds = ds; }
    @Override
    public void run(String... args) throws Exception {
        logger.info("DataSource: " + ds.toString());
    }
}
// Application.Java
@SpringBootApplication
public class Application {
    private DataSource ds;
    @Autowired
    public Application(DataSource ds) { this.ds = ds; }
    public static void main(String... args) {
        SpringApplication.run(Application.class, args); 
    }
    @Bean
    public MyCommandLineRunner schedulerRunner() {
        return new MyCommandLineRunner(ds);
    }
}

Le code ci-dessus donne le même résultat, mais ne signale aucun avertissement dans IntelliJ. Je suis confus, en quoi le deuxième code est-il meilleur que le premier? Suis-je en train de suivre une logique incorrecte? Doit-il être câblé différemment?

En bref, quelle est la bonne façon de procéder?

note DataSource n'est qu'un pur exemple, cette question s'applique à tout ce qui est câblé automatiquement.

note 2 Juste pour dire que MyCommandLineRunner.Java ne peut pas avoir un autre constructeur vide, car DataSource doit être câblé/initialisé automatiquement. Il signalera une erreur et ne sera pas compilé.

16
NemanjaT

Il existe plusieurs façons de l'améliorer.

  1. Vous pouvez supprimer @Autowired depuis votre MyCommandLineRunner lorsque vous laissez un @Bean la méthode en construit une instance. Injectez le DataSource directement dans la méthode comme argument.

  2. Ou supprimez @Autowired et supprimez le @Bean et gifler un @Component annotation sur votre MyCommandLineRunner pour la détecter et supprimer la méthode d'usine.

  3. Intégrez votre MyCommandLineRunner à l'intérieur de votre @Bean méthode comme lambda.

Pas de câblage automatique dans le MyCommandLineRunner

public class MyCommandLineRunner implements CommandLineRunner {
    private final Log logger = LogFactory.getLog(getClass());
    private final DataSource ds;

    public MyCommandLineRunner(DataSource ds) { this.ds = ds; }

    @Override
    public void run(String... args) throws Exception {
        logger.info("DataSource: " + ds.toString());
    }
}

Et la classe d'application.

@SpringBootApplication
public class Application {

    public static void main(String... args) {
        SpringApplication.run(Application.class, args); 
    }

    @Bean
    public MyCommandLineRunner schedulerRunner(DataSource ds) {
        return new MyCommandLineRunner(ds);
    }
}

L'utilisation de @Component

@Component
public class MyCommandLineRunner implements CommandLineRunner {
    private final Log logger = LogFactory.getLog(getClass());
    private final DataSource ds;

    public MyCommandLineRunner(DataSource ds) { this.ds = ds; }

    @Override
    public void run(String... args) throws Exception {
        logger.info("DataSource: " + ds.toString());
    }
}

Et la classe d'application.

@SpringBootApplication
public class Application {

    public static void main(String... args) {
        SpringApplication.run(Application.class, args); 
    }

}

Inline CommandLineRunner

@SpringBootApplication
public class Application {

    private static final Logger logger = LoggerFactory.getLogger(Application.class)

    public static void main(String... args) {
        SpringApplication.run(Application.class, args); 
    }

    @Bean
    public MyCommandLineRunner schedulerRunner(DataSource ds) {
        return (args) -> (logger.info("DataSource: {}", ds); 
    }
}

Tous ces éléments sont des moyens valables de construire vos instances. Lequel utiliser, utilisez celui avec lequel vous vous sentez à l'aise. Il y a plus d'options (toutes les variantes de celles mentionnées ici).

8
M. Deinum

Pensez à rendre le champ ds final, alors vous n'avez pas besoin de @Autowired. En savoir plus sur l'injection de dépendance http://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-spring-beans-and-dependency-injection.html#using-boot -spring-beans-and-dependency-injection

Pour garder le code propre, avez-vous envisagé d'utiliser des annotations Lombok? @RequiredArgsConstructor(onConstructor = @__(@Autowired)) générerait le constructeur avec des annotations @Autowired. Voir plus ici https://projectlombok.org/features/Constructor.html

Votre code pourrait ressembler à ceci:

@Slf4j
@RequiredArgsConstructor
// MyCommandLineRunner.Java
public class MyCommandLineRunner implements CommandLineRunner {

    //final fields are included in the constructor generated by Lombok
    private final DataSource ds;

    @Override
    public void run(String... args) throws Exception {
        log.info("DataSource: {} ", ds.toString());
    }
}

// Application.Java
@SpringBootApplication
@RequiredArgsConstructor(onConstructor_={@Autowired}) // from JDK 8
// @RequiredArgsConstructor(onConstructor = @__(@Autowired)) // up to JDK 7
public class Application {

    private final Datasource ds;

    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean 
    public MyCommandLineRunner schedulerRunner() {
        return new MyCommandLineRunner(ds);
    }
}

Édition ultérieure

La solution sans Lombok s'appuie sur Spring pour injecter la dépendance lors de la création du bean

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    /**
     * dependency ds is injected by Spring
     */
    public MyCommandLineRunner schedulerRunner(DataSource ds) {
        return new MyCommandLineRunner(ds);
    }
}

// MyCommandLineRunner.Java
public class MyCommandLineRunner implements CommandLineRunner {
    private final Log logger = LogFactory.getLog(getClass());

    private final DataSource ds;

    public MyCommandLineRunner(DataSource ds){
        this.ds = ds;
    }

    @Override
    public void run(String... args) throws Exception {
        logger.info("DataSource: "+ ds.toString());
    }
}
3
user7757360