本文共享自华为云社区《Spring高手之路8——Spring Bean模块安装的艺术:@Import详解》,作者: 砖业洋__。

本文将带你深入探索Spring结构的安装机制,以及它如何使你的代码更具模块化和灵活性。咱们首要介绍Spring手动安装的根底知识,然后进一步解析@Import注解在模块安装中的关键角色。文章包含从导入一般类、装备类,到运用ImportSelector和ImportBeanDefinitionRegistrar进行动态和选择性安装等多个层次,旨在协助读者全面理解和把握Spring的安装技能。

1. Spring手动安装根底

Spring中,手动安装通常是指经过XML装备文件明确指定Bean及其依靠,或许在代码中直接运用new关键字创立目标并设定依靠联系。

但是,随着Spring 2.0引进注解,以及Spring 3.0全面支撑注解驱动开发,这个进程变得更加主动化。例如,经过运用@Component + @ComponentScanSpring能够主动地找到并创立bean,经过@AutowiredSpring能够主动地注入依靠。这种办法被称为 “主动安装”。

关于手动安装,最常见的场景或许是在不运用Spring的上下文的单元测试或许简略的POJO类中,经过new关键字直接创立目标和设定依靠联系。比方下面这段代码:

public class Main {
    public static void main(String[] args) {
        ServiceA serviceA = new ServiceA();
        ServiceB serviceB = new ServiceB(serviceA);
        //...
    }
}

在这个比方中,咱们显式地创立了ServiceAServiceB的目标,并将ServiceA的目标作为依靠传递给了ServiceB。这便是一个典型的手动安装的比方。

需求留意的是,手动安装的运用通常是有限的,因为它需求开发者显式地在代码中办理目标的创立和依靠联系,这在大型运用中或许会变得非常杂乱和难以办理。因而,Spring的主动安装机制(例如@Autowired注解,或许@Configuration@Bean的运用)通常是更常见和推荐的办法。

2. Spring结构中的模块安装

模块安装便是将咱们的类或许组件注册到SpringIoCInversion of Control,操控反转)容器中,以便于Spring能够办理这些类,并且在需求的时分能够为咱们主动地将它们注入到其他的组件中。

Spring结构中,有多种办法能够完结模块安装,包含:

  1. 依据Java的装备:经过运用@Configuration@Bean注解在Java代码中界说的Bean。这是一种声明式的办法,咱们能够明确地操控Bean的创立进程,也能够运用@Value@PropertySource等注解来处理装备属性。

  2. 依据XML的装备Spring也支撑经过XML装备文件界说Bean,这种办法在前期的Spring版别中更常见,但现在依据Java的装备办法更为干流。

  3. 依据注解的组件扫描:经过运用@Component@Service@Repository@Controller等注解以及@ComponentScan来主动检测和注册Bean。这是一种隐式的办法,Spring会主动扫描指定的包来查找带有这些注解的类,并将这些类注册为Bean

  4. 运用@Import:这是一种显式的办法,能够经过它直接注册类到IOC容器中,无需这些类带有@Component或其他特别注解。咱们能够运用它来注册一般的类,或许注册完结了ImportSelectorImportBeanDefinitionRegistrar接口的类,以供给更高级的安装能力。

每种办法都有其运用场景,依据具体的需求,咱们能够选择适宜的办法来完结模块安装。比方在Spring Boot中,咱们日常开发或许会更多地运用依据Java的装备和依据注解的组件扫描来完结模块安装。

2.1 @Import注解简略运用

@Import是一个强壮的注解,它为咱们供给了一个快速、方便的办法,使咱们能够将需求的类或许装备类直接安装到Spring IOC容器中。这个注解在模块安装的上下文中特别有用。

咱们先来看一下简略的运用,后边再具体介绍

悉数代码如下:

Book.java

package com.example.demo.bean;
public class Book {
    private String name;
    public Book() {
        this.name = "Imported Book";
    }
    public String getName() {
        return name;
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                '}';
    }
}

LibraryConfig.java

package com.example.demo.configuration;
import com.example.demo.bean.Book;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(Book.class)
public class LibraryConfig {
}

运用 @Import 注解来导入一个一般的类(即一个没有运用 @Component 或许 @Service 之类的注解符号的类),Spring 会为该类创立一个 Bean,并且这个 Bean 的姓名默许便是这个类的全限制类名。

主程序:

package com.example.demo;
import com.example.demo.bean.Book;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean(Book.class);
        System.out.println(book);
    }
}

运转成果如下:

@Import :Spring Bean模块装配的艺术

3. @Import模块安装的四种办法

3.1 @Import注解的功用介绍

Spring中,有时分咱们需求将某个类(或许是一个一般类,或许是一个装备类等等)导入到咱们的运用程序中。Spring供给了四种首要的办法来完结这个任务,后边咱们会分别解释。

@Import注解能够有以下几种运用办法:

  • 导入一般类:能够将一般类(没有被@Component或许@Service等注解标示的类)导入到SpringIOC容器中,Spring会为这个类创立一个Bean,这个Bean的姓名默许为类的全限制类名。

  • 导入装备类:能够将一个或多个装备类(被@Configuration注解标示的类)导入到SpringIOC容器中,这样咱们就能够一次性地将这个装备类中界说的一切Bean导入到SpringIOC容器中。

  • 运用ImportSelector接口:假如想动态地导入一些BeanSpringIOC容器中,那么能够完结ImportSelector接口,然后在@Import注解中引进ImportSelector完结类,这样Spring就会将ImportSelector完结类回来的类导入到SpringIOC容器中。

  • 运用ImportBeanDefinitionRegistrar接口:假如想在运转时动态地注册一些BeanSpringIOC容器中,那么能够完结ImportBeanDefinitionRegistrar接口,然后在@Import注解中引进ImportBeanDefinitionRegistrar完结类,这样Spring就会将ImportBeanDefinitionRegistrar完结类注册的Bean导入到SpringIOC容器中。

@Import注解首要用于手动安装,它能够让咱们显式地导入特定的类或许其他装备类到SpringIOC容器中。特别是当咱们需求引进第三方库中的类,或许咱们想要显式地操控哪些类被安装进SpringIOC容器时,@Import注解会非常有用。它不仅能够直接导入一般的 Java 类并将其注册为 Bean,还能够导入完结了 ImportSelectorImportBeanDefinitionRegistrar 接口的类。这两个接口供给了更多的灵活性和操控力,使得咱们能够在运转时动态地注册 Bean,这是经过 @Configuration + @Bean 注解组合无法做到的。

例如,经过 ImportSelector 接口,能够在运转时决议需求导入哪些类。而经过 ImportBeanDefinitionRegistrar 接口,能够在运转时操控 Bean 的界说,包含 Bean 的称号、效果域、结构参数等等。

尽管 @Configuration + @Bean 在许多情况下都足够运用,但 @Import 注解由于其更大的灵活性和操控力,在处理更杂乱的场景时,或许会是一个更好的选择。

3.2 导入一般类与自界说注解的运用

咱们第2节的比方也是导入一般类,这儿加一点难度,延伸到自界说注解的运用。

布景:图书馆模块安装

在这个比方中,咱们将创立一个图书馆系统,包含图书馆(Library)类、图书馆办理员(Librarian)类、图书(Book)类,还有书架(BookShelf)类。咱们的目标是创立一个图书馆,并将一切组件安装到一同。

首要,咱们创立一个自界说@ImportLibrary注解,经过此注解咱们将把一切相关的类安装到图书馆里边:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({Librarian.class, Book.class, BookShelf.class})
public @interface ImportLibrary {
}

这个@ImportLibrary注解内部实际上运用了@Import注解。当Spring处理@Import注解时,会将其参数指定的类增加到Spring运用上下文中。当咱们在Library类上运用@ImportLibrary注解时,Spring会将Librarian.classBook.classBookShelf.class这三个类增加到运用上下文中。

然后,咱们创立图书馆办理员(Librarian)、图书(Book)、书架(BookShelf)这三个类:

Librarian.java

package com.example.demo.bean;
public class Librarian {
    public void manage() {
        System.out.println("The librarian is managing the library.");
    }
}

Book.java

package com.example.demo.bean;
public class Book {
    private String name;
    // @ImportLibrary里边有@Import会主动安装,会调用无参结构,不写会报错
    public Book() {
    }
    public Book(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

BookShelf.java

package com.example.demo.bean;
import java.util.List;
public class BookShelf {
    private List<Book> books;
    // @ImportLibrary里边有@Import会主动安装,会调用无参结构,不写会报错
    public BookShelf() {
    }
    public BookShelf(List<Book> books) {
        this.books = books;
    }
    public List<Book> getBooks() {
        return books;
    }
}

最终,咱们创立一个图书馆(Library)类,并在这个类上运用咱们刚刚创立的@ImportLibrary注解:

package com.example.demo.configuration;
import com.example.demo.annotations.ImportLibrary;
import com.example.demo.bean.Book;
import com.example.demo.bean.BookShelf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
@ImportLibrary
@Configuration
public class Library {
    @Bean
    public Book book1() {
        return new Book("The Catcher in the Rye");
    }
    @Bean
    public Book book2() {
        return new Book("To Kill a Mockingbird");
    }
    @Bean
    public BookShelf bookShelf(Book book1, Book book2) {
        return new BookShelf(Arrays.asList(book1, book2));
    }
}

然后咱们能够创立一个发动类并初始化IOC容器,看看是否能够成功获取到Librarian类、BookShelf类和Book类的实例:

package com.example.demo;
import com.example.demo.bean.Book;
import com.example.demo.bean.BookShelf;
import com.example.demo.bean.Librarian;
import com.example.demo.configuration.Library;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Library.class);
        // 这行代码供调试查看运用
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        Librarian librarian = context.getBean(Librarian.class);
        BookShelf bookShelf = context.getBean("bookShelf", BookShelf.class);
        Book book1 = (Book) context.getBean("book1");
        Book book2 = (Book) context.getBean("book2");
        librarian.manage();
        bookShelf.getBooks().forEach(book -> System.out.println("Book: " + book.getName()));
    }
}

这个比方中,咱们经过@Import注解一次性把LibrarianBookBookShelf这三个类导入到了SpringIOC容器中,这便是模块安装的强壮之处。

调试成果

@Import :Spring Bean模块装配的艺术

当咱们运用 @Import 注解来导入一个一般的类(即一个没有运用 @Component 或许 @Service 之类的注解符号的类),Spring 会为该类创立一个 Bean,并且这个 Bean 的姓名默许便是这个类的全限制类名。

运转成果:

@Import :Spring Bean模块装配的艺术

3.3 导入装备类的战略

这儿运用Spring@Import注解导入装备类,咱们将创立一个BookConfig类和LibraryConfig类,然后在主运用类中获取Book实例。

悉数代码如下:

创立一个装备类BookConfig,用于创立和装备Book实例:

package com.example.demo.configuration;
import com.example.demo.bean.Book;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BookConfig {
    @Bean
    public Book book() {
        Book book = new Book();
        book.setName("Imported Book");
        return book;
    }
}

在这儿,咱们界说了一个Book类:

package com.example.demo.bean;
public class Book {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

创立一个装备类LibraryConfig,运用@Import注解来导入BookConfig类:

package com.example.demo.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(BookConfig.class)
public class LibraryConfig {
}

主程序:

package com.example.demo;
import com.example.demo.bean.Book;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean(Book.class);
        System.out.println(book.getName());
    }
}

运转成果:

@Import :Spring Bean模块装配的艺术

在这个比方中,当Spring容器发动时,它会经过@Import注解将BookConfig类导入到Spring 上下文中,并创立一个Bean。然后咱们能够在主程序中经过context.getBean(Book.class)获取到Book的实例,并打印出书名。

3.4 运用ImportSelector进行选择性安装

假如咱们想动态地选择要导入的类,咱们能够运用一个ImportSelector完结。

悉数代码如下:

界说一个 Book 类:

package com.example.demo.bean;
public class Book {
    private String name = "java从入门到通晓";
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

创立图书馆办理员Librarian

package com.example.demo.bean;
public class Librarian {
    public void manage() {
        System.out.println("The librarian is managing the library.");
    }
}

界说一个 BookImportSelector,完结 ImportSelector 接口:

package com.example.demo.configuration;
import com.example.demo.bean.Librarian;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class BookImportSelector implements ImportSelector {
    /**
     * 这儿演示2种办法,一种是拿到class文件后getName,一种是直接写全限制类名
     * @param importingClassMetadata
     * @return
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] { "com.example.demo.bean.Book", Librarian.class.getName() };
    }
}

ImportSelector接口能够在运转时动态地选择需求导入的类。完结该接口的类需求完结selectImports办法,这个办法回来一个字符串数组,数组中的每个字符串代表需求导入的类的全类名,咱们能够直接在这儿将 Book 类和 Librarian 类加入到了 Spring 容器中。

运用Class.getName()办法获取全限制类名的办法,比直接硬编码类的全名为字符串更推荐,原因如下:

  1. 防止过错:假如类名或包名有所改动,硬编码的字符串或许不会跟随变化,这或许导致过错。而运用Class.getName()办法,则会随类的改动主动更新,防止此类过错。
  2. 代码明晰:运用Class.getName()能让读代码的人更清楚地知道你是要引用哪一个类。
  3. 增强代码的可读性和可维护性:运用类的字节码获取全限制类名,使得代码阅读者能够明晰地知道这是什么类,增加了代码的可读性。一起,也方便了代码的维护,因为在修改类名或许包名时,不需求手动去修改硬编码的类名。

界说一个装备类 LibraryConfig,运用 @Import 注解导入 BookImportSelector

package com.example.demo.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(BookImportSelector.class)
public class LibraryConfig {
}

创立一个主运用类,从 SpringAnnotationConfigApplicationContext 中获取 Book 的实例:

package com.example.demo;
import com.example.demo.bean.Book;
import com.example.demo.bean.Librarian;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean(Book.class);
        Librarian librarian = context.getBean(Librarian.class);
        System.out.println(book.getName());
        librarian.manage();
    }
}

运转成果:

@Import :Spring Bean模块装配的艺术

Spring Boot 中,ImportSelector 被大量运用,尤其在主动装备(auto-configuration)机制中起着关键效果。例如,AutoConfigurationImportSelector 类便是间接完结了 ImportSelector,用于主动导入一切 Spring Boot 的主动装备类。

咱们通常会在Spring Boot发动类上运用 @SpringBootApplication 注解,实际上,@SpringBootApplication 注解中也包含了 @EnableAutoConfiguration@EnableAutoConfiguration 是一个复合注解,它的完结中导入了一般类 @Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector 类间接完结了 ImportSelector接口,用于主动导入一切 Spring Boot 的主动装备类。

如下图:

@Import :Spring Bean模块装配的艺术

3.5 运用ImportBeanDefinitionRegistrar进行动态安装

ImportBeanDefinitionRegistrar接口的首要功用是在运转时动态的往Spring容器中注册Bean,完结该接口的类需求重写registerBeanDefinitions办法,这个办法能够经过参数中的BeanDefinitionRegistry接口向Spring容器注册新的类,给运用供给了更大的灵活性。

悉数代码如下:

首要,界说一个 Book 类:

package com.example.demo.bean;
public class Book {
    private String name = "java从入门到通晓";
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

界说一个 BookRegistrar 类,完结 ImportBeanDefinitionRegistrar 接口:

package com.example.demo.configuration;
import com.example.demo.bean.Book;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
public class BookRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Book.class);
        // 经过反射技能调用setter办法给name赋值,也能够在结构器赋值name,name需求调用beanDefinitionBuilder.addConstructorArgValue("战争与和平");
        beanDefinitionBuilder.addPropertyValue("name", "战争与和平");
        BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("myBook", beanDefinition);
    }
}

下面来具体解释一下BookRegistrar类里边的registerBeanDefinitions办法和参数。

  • AnnotationMetadata importingClassMetadata: 这个参数表明当前被@Import注解导入的类的一切注解信息,它包含了该类上一切注解的具体信息,比方注解的称号,注解的参数等等。

  • BeanDefinitionRegistry registry: 这个参数是SpringBean界说注册类,咱们能够经过它往Spring容器中注册Bean。在这儿,咱们运用它来注册咱们的Book Bean

在办法registerBeanDefinitions中,咱们创立了一个BeanDefinition,并将其注册到SpringBeanDefinitionRegistry中。

代码首要经过BeanDefinitionBuilder.genericBeanDefinition(Book.class)创立一个BeanDefinitionBuilder实例,这个实例用于构建一个BeanDefinition。咱们运用addPropertyValue("name", "战争与和平")为该BeanDefinition增加一个name属性值。

接着咱们经过beanDefinitionBuilder.getBeanDefinition()办法得到BeanDefinition实例,并设置其效果域为原型效果域,这表明每次从Spring容器中获取该Bean时,都会创立一个新的实例。

最终,咱们将这个BeanDefinition以姓名 "myBook" 注册到BeanDefinitionRegistry中。这样,咱们就能够在Spring容器中经过姓名 "myBook" 来获取咱们的Book类的实例了。

接着界说一个装备类 LibraryConfig,运用 @Import 注解导入 BookRegistrar

package com.example.demo.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import(BookRegistrar.class)
public class LibraryConfig {
}

创立一个主运用类,从 Spring ApplicationContext 中获取 Book 的实例:

package com.example.demo;
import com.example.demo.bean.Book;
import com.example.demo.configuration.LibraryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfig.class);
        Book book = context.getBean("myBook", Book.class);
        System.out.println(book.getName());
    }
}

运转成果:

@Import :Spring Bean模块装配的艺术

在这个比方中,咱们运用 AnnotationConfigApplicationContext 初始化 Spring 容器并供给装备类。然后经过 context.getBean("book", Book.class)Spring 容器中获取名为 book 的实例。

ImportBeanDefinitionRegistrar接口供给了非常大的灵活性,咱们能够依据自己的需求编写任何需求的注册逻辑。这关于构建杂乱的、高度定制的Spring运用是非常有用的。

Spring Boot就广泛地运用了ImportBeanDefinitionRegistrar。例如,它的@EnableConfigurationProperties注解便是经过运用一个ImportBeanDefinitionRegistrar来将装备属性绑定到Beans上的,这便是ImportBeanDefinitionRegistrar在实践中的一个实际运用的比方。

点击关注,第一时间了解华为云新鲜技能~