Spring泛型依赖注入

Spring 4.0版本中更新了很多新功能,其中比较重要的一个就是对带泛型Bean进行依赖注入的支持。Spring4的这个改动使得代码可以利用泛型进行进一步的精简优化。

泛型依赖注入的优点

泛型依赖注入就是允许我们在使用spring进行依赖注入的同时,利用泛型的优点对代码进行精简,将可重复使用的代码全部放到一个类之中,方便以后的维护和修改。同时在不增加代码的情况下增加代码的复用性。下面我们用一个例子来简单讲解一下泛型的优点:

假设我们已经定义好了两个实体类StudentFaculty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Student {
private int id;
private String name;
private double grade;

public Student(int id, String name, double grade) {
this.id = id;
this.name = name;
this.grade = grade;
}

@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", grade=" + grade + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Faculty {
private int id;
private String name;
private String evaluation;

public Faculty(int id, String name, String evaluation) {
this.id = id;
this.name = name;
this.evaluation = evaluation;
}

@Override
public String toString() {
return "Faculty [id=" + id + ", name=" + name + ", evaluation=" + evaluation + "]";
}
}

然后我们需要持久层Bean来调用StudentFaculty里面的方法。在Spring支持泛型依赖注入之前,我们需要为两个实体类分别定义一个持久层Bean,然后从数据库获取我们的实体类,再调用实体类中的方法。使用这种原始方法的代码如下:

1
2
3
4
5
6
7
8
9
public class StudentRepository {	
public Student getBean(String beanName) {
//获取对应的Student
}

public void printString(Student s) {
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
public class FacultyRepository {	
public Faculty getBean(String beanName) {
//获取对应的Faculty
}

public void printString(Faculty f) {
System.out.println(f);
}
}

大家可以看到,这样的代码每个实体类都需要编写一个新的持久层Bean,每一个持久层Bean中的实体类类型都是写死的,复用性很差。更重要的是,由于每个持久层Bean中所包含的实体类不同,持久层Bean中重复的方法(如上面例子中的printString)需要在每一个持久层Bean中都实现一次,这大大增加了代码的维护成本。

当然,有一些方法可以部分解决这个问题。比如我们可以定义一个持久层Bean的父类BaseRepository,然后在里面编写一个通用的pirntString方法:

1
2
3
4
5
public class BaseRepository {
public void printString(Object o) {
System.out.println(o);
}
}

接着,我们可以在各个持久层Bean中调用BaseRepository的方法来实现printString:

1
2
3
4
5
6
7
8
9
public class StudentRepository extends BaseRepository{	
public Student getBean(String beanName) {
//获取对应的Student
}

public void printString(Student s) {
super.printString(s);
}
}

这样的话,printString的实现实际上只编写了一遍,因此我们提高了代码的复用性。同时,当printString方法不是简单的打印到控制台,而具有复杂的代码和逻辑时,我们可以把代码全部放在BaseRepository中,方便以后的修改和维护。但是,这种方法仍然要求每一个持久层Bean编写一个printSring方法来调用父类的方法,尽管这个方法只有简单的一行,当类似的方法多起来之后代码的数量还是很可观的。

除了加入父类之外,还有一些其他的方法可以减少代码量,提高代码的复用性。比如我们可以在父类中加入setter方法使得业务层可以为持久层手工注入实体类的类别(如Student.class),但是并没有非常好的解决方案。

但是当我们使用泛型时,这些问题就迎刃而解了。我们只需要定义一个持久层BeanBaseRepository,也就是上面例子中的父类,而不需要任何子类:

1
2
3
4
5
6
7
8
9
public class BaseRepository<T> {
public T getBean(String beanName) {
//获取对应的t
}

public void printString(T t) {
System.out.println(t);
}
}

这个持久层Bean可以包含所有我们在持久层想要复用的方法。通过泛型,我们的持久层代码可以用在所有实体类身上,并且我们还可以通过继承方便的添加某些实体类特有的方法。我们没有增加额外的代码,但是提高了代码复用程度,同时我们把可重复使用的代码全部集中起来,方便了以后的维护和修改。

上面所讲的内容都是泛型本身的优点,和Spring 4.0的泛型依赖注入并没有直接联系。但是,Spring 4.0开始支持的泛型依赖注入对于我们使用泛型非常重要:在Spring 4.0之前,Spring的依赖注入功能是不能自动识别上面例子中泛型的类,而给不同的持久层Bean加以区分的。因此在Spring 4.0之前,BaseRepository<Student>BaseRepository<Faculty>会被认为是同一类型,一般需要用名字等其他方式加以区分。但是现在,Spring会正确的识别声明的泛型类别,并且根据泛型给持久层Bean进行分类。所以Student和Faculty的持久层Bean可以被正确的区分,并且注入到上一层。这为我们在代码中使用泛型提供了极大的便利。

泛型依赖注入的实现

下面我们就来看看使用泛型的Bean的依赖注入应该如何实现:

使用泛型Bean的依赖注入与普通Bean的依赖注入在实现方法上基本相同,同样可以通过xml配置文件和注解两种方式进行依赖注入。但是由于泛型中尖括号(“<>”)的存在,使得xml配置文件依赖注入过程中会出现编译报错的情况。有的编译器即使对尖括号进行转义也依然会报错。因此,为了避免不必要的麻烦,建议大家使用注解的方式进行带泛型Bean的依赖注入。

使用注解进行依赖注入有如下几种方式:

使用注解@Configuration进行依赖注入

与普通Bean一样,我们可以利用注解@Configuration来声明我们需要的使用泛型的Bean,并且进行依赖注入。

首先我们需要新建一个类,作为我们的配置类:

1
2
3
4
@Configuration
public class MyConfiguration {

}

其中,注解@Configuration的作用是告诉spring这个类是一个配置类,这样Spring就会自动扫描这个类中声明的所有Bean,并把它们加入Spring容器中。不过在此之前,我们需要在spring的配置文件中添加component-scan:

1
<context:component-scan base-package="com.somepackage.**" ></context:component-scan>

之后注解@configuration才会被扫描到,里面声明的Bean才会被添加进Spring容器中
在这之后,我们就可以使用注解对带泛型的Bean进行依赖注入了。

首先,我们需要声明两个需要用到的持久层Bean,一个是Student的持久层Bean,另外一个是Faculty的。只有在声明了这两个Bean并且添加到Spring容器中后,Spring才能为我们进行依赖注入。

在配置类中声明这两个Bean的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MyConfiguration {
@Bean
public BaseRepository<Student> studentRepository() {
return new BaseRepository<Student>() {};
}

@Bean
public BaseRepository<Faculty> facultyRepository() {
return new BaseRepository<Faculty>() {};
}
}

其中,注解@Bean与Spring的正常使用方法相同,就是声明一个新的Bean。Spring在扫描配置类时,就会把这里声明的Bean加入到Spring容器中,供以后使用。这里每个Bean的名称就是方法名,如studentRepository,而Bean的类型就是返回的Object的类型(不是方法的返回类型,方法的返回类型可以是Interface等不能实例化的类型)。

如果你还需要声明其他的Bean,比如你不需要从数据库获取数据,也可以把它们加入到这个配置类中。

然后我们就可以定义我们的业务层Bean,并且用业务层Bean调用持久层的方法来对数据进行操作。我们这里使用printString方法作为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class ExampleService {
@Autowired private BaseRepository<Student> studentRepo; //自动注入BaseRepository<Student>() {}
@Autowired private BaseRepository<Faculty> facultyRepo; //自动注入BaseRepository<Faculty>() {}

public void test() {
Student s = studentRepo.getBean("studentBean");
studentRepo.printString(s);

Faculty f = facultyRepo.getBean("facultyBean");
facultyRepo.printString(f);
}
}

在业务层中,我们可以使用注解@Autowired进行依赖注入。@Autowired默认按照字段的类进行依赖注入,而Spring4的新特性就是把泛型的具体类型(如上文业务层中BaseRepository<Student>中的Student)也作为类的一种分类方法(Qualifier)。这样我们的studentRepo和facultyRepo虽然是同一个类BaseRepository,但是因为泛型的具体类型不同,也会被区分开。

这里我先创建了两个实体类实例,并且加入到了刚才提到的配置类中。这样这两个Bean就会被加入到Spring容器之中,而我们可以在getBean方法当中获取他们。这两个Bean的名字分别是studentBeanfacultyBean,与业务层Bean中填写的名字保持一致:

1
2
3
4
5
6
7
8
9
@Bean
public Student studentBean() {
return new Student(1, "Anna", 3.9);
}

@Bean
public Faculty facultyBean() {
return new Faculty(2, "Bob", "A");
}

当然,如果你有其他方法能够获取到实体类,比如你的工程整合了Hibernate ORM或者其他工具来连接数据库,就不需要向Spring容器中加入对应的Bean了,getBean方法的实现也可以相应的改变。我这里只是用这两个实体类的Bean作为例子。

然后当我们调用业务层的test方法时,控制台打印的结果是:

1
2
Student [id=1, name=Anna, grade=3.9]
Faculty [id=2, name=Bob, evaluation=A]

而当我们在业务层里试图错误的调用方法:

1
facultyRepo.printString(s);

的时候,会出现编译错误。

读到这里,可能有的人已经发现了,这个例子存在两个疑点。第一,这个例子不能证明我们在运行期成功实现了依赖注入,因为我们在运行期为printString方法传入了Student和Faculty的实例。第二,我们在声明Bean的时候,声明的类不是BaseRepository,而是BaseRepository的一个匿名子类。

为了解答这两个问题,我在BaseRepository中定义了一个新的方法:

1
2
3
4
5
public void printType() {
Type genericSuperclass = this.getClass().getGenericSuperclass();
Class<T> entityClass = (Class<T>) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
System.out.println(entityClass);
}

这段代码前两行的作用是在带泛型的类中,在运行期确定泛型T的类。如果需要在BaseRepository中使用到T在运行期的具体类型,就应该使用这个方法来获取。下面是一个比较详细的解释:

由于java的擦除机制,泛型T只在编译期有效,在运行期会被擦除,所以我们在运行期不能直接获得T的类,一些对一般类有效的方法,比如T.class和t.getClass()对泛型都是非法的。因此我们需要通过反射机制获取T的类。

这段代码第一行的作用是获取当前类的父类,然后在第二行中通过查看父类的类参数获得当前类泛型的类。也就是说,这是一个通过父类的参数来查看子类中泛型的具体类型的方法。因此,这个方法有一些使用上的要求:首先,这个方法必须被带泛型类的子类所使用,带泛型类本身是不能使用这个方法的。另外,这个子类在泛型的位置必须继承一个具体的类,而不是泛型T。举个例子,当有一个类继承BaseBaen<A>时,这个方法就可以使用,而继承BaseBean<T>的时候就不能使用,因为A是具体的类,而T是泛型,在运行期就算我们从父类中取到了T,因为有擦除机制,我们仍然无法得知T是一个什么类。

值得一提的是,Spring4也是通过同样的方法添加了对泛型依赖注入的支持。因此我们如果想使用Spring4的新功能,在定义Bean的时候就必须定义为泛型类的子类,如上面例子中的new BaseBean<A>() {}。这个Bean是BaseBean的一个匿名子类,继承的是BaseBean<A>。这样的话Spring就可以正确获取到泛型T的类(A),并且以此为根据帮助我们实行依赖注入。

Spring的文档和源代码里都有关于注解依赖注入的说明,大家有兴趣的话可以去看一下。

与此同时,我们会发现上面的printType方法是不接收任何实例的,因此这个方法可以帮我们判断泛型的依赖注入是否成功。为了测试,我对业务层的test方法进行了如下修改:

1
2
3
4
5
6
7
8
9
public void test() {
Student s = studentRepo.getBean("studentBean");
//studentRepo.printString(s);
studentRepo.printType();

Faculty f = facultyRepo.getBean("facultyBean");
//facultyRepo.printString(f);
facultyRepo.printType();
}

然后当我们调用test方法进行测试时,控制台会打印以下信息:

1
2
class com.somepackage.Student
class com.somepackage.Faculty

这些信息说明我们在没有传入实例的情况下也正确获取到了泛型T的类,泛型的依赖注入成功了。

  1. 前文提到的@Bean注解声明Bean的方法也可以使用在@Component注解标注的类当中,但是Spring建议这种做法只在工厂类中使用,并不建议大规模使用。另外,Spring对不在@Component注解标注的配置类中声明的Bean的关联上有一些限制,详细的情况请参照Spring文档。关于注解@Component的正确使用方法,请看下一小节。

  2. 除了使用@Autowired注解进行依赖注入外,我们还可以使用@Resource注解进行依赖注入。因为@Resource是优先根据名字进行依赖注入,我们最好让字段的名字与Bean名字相同。

使用注解@Component等进行依赖注入

上一小节我们讲述了利用@Configuration注解标注的配置类进行泛型依赖注入的实现方法和部分原理。其中我们提到,如果想要Spring的泛型依赖注入成功,我们必须把Bean定义为使用泛型的类的子类。而定义一个子类最常见的方法是定义一个新的类,然后进行继承。

因此,我们可以使用@Component注解以及它的子注解(如@Controller,@Service和@Repository)来声明一个新的Bean。如上一小节所说,这个子类在泛型的位置必须继承一个具体的类型,而不能继承泛型T,否则Spring的自动依赖注入不会成功。

除此之外,这些注解的使用方法都与没有泛型时完全相同,下面我们就来看一下具体的代码:

我为前面的两种BaseRepository编写了两个子类StudentRepository和FacultyRepository:

1
2
3
4
@Repository
public class StudentRepository extends BaseRepository<Student> {

}
1
2
3
4
@Repository
public class FacultyRepository extends BaseRepository<Faculty> {

}

并且使用注解@Repository进行标注。这样的话,Spring在扫描时将会扫描到这两个类,并创建两个对应的Bean加入到Spring容器中。当然,如果你想要正确使用Spring的自动扫描功能,需要在Spring配置文件中加入component-scan,详细的做法请参考上一小节。

需要注意的是,使用了@Repository注解就已经往Spring容器中加入了一个Bean。因此,如果你在上一小节编写了@Configuration配置类,请务必把@Configuration注解注释掉,让Spring不再扫描这个配置类,或者把配置类中两个持久层Bean的@Bean注解注释掉,让Spring不再扫描这两个持久层。否则Spring在使用@Autowired注解进行依赖注入时会因为同一类型的Bean有两个而报错。

下面是我们的业务层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExampleService {
@Autowired private BaseRepository<Student> studentRepo; //自动注入BaseRepository<Student>() {}
@Autowired private BaseRepository<Faculty> facultyRepo; //自动注入BaseRepository<Faculty>() {}

public void test() {
Student s = studentRepo.getBean("studentBean");
studentRepo.printString(s);
studentRepo.printType();

Faculty f = facultyRepo.getBean("facultyBean");
facultyRepo.printString(f);
facultyRepo.printType();
}
}

业务层的代码与上一小节相同,没有做任何修改。注意在需要依赖注入的两个字段中,我们声明的类型仍然是使用泛型的类BaseRepository,而不是我们刚才定义的子类StudentRepository和FacultyRepository。实际上,我们根本不需要知道这些子类的类型,就可以调用子类的方法,这正是Spring依赖注入的强大之处。

当我们调用test方法时,控制台会打印出以下信息:

1
2
3
4
Student [id=1, name=Anna, grade=3.9]
class com.somepackage.Student
Faculty [id=2, name=Bob, evaluation=A]
class com.somepackage.Faculty

从这些信息我们可以看到,Spring在声明Bean的类型与依赖注入目标类型不同的情况下也可以成功注入。这是因为Spring4开始将泛型的具体类型作为Bean分类的一种方法(Qualifier),因此Spring能够成功区分BaseRepository<Student>和BaseRepository<Faculty>,以及他们的子类。

但是,这个例子也存在一个问题:因为依赖注入的地方声明的是父类BaseRepository,我们如何判定Spring为我们注入的是子类StudentRepository和FacultyRepository,还是父类BaseRepository呢?实际上我们根本不用担心这个问题,因为我们根本没有声明任何父类BaseRepository类型的Bean,只声明了子类类型的Bean。所以如果Spring依赖注入成功了,就一定注入的是子类类型的Bean。但是在这里,我们也通过代码验证一下我们的这个猜想。

为了进行验证,我在StudentRepository和FacultyRepository中覆盖了父类BaseRepository的printString方法:

1
2
3
4
5
6
7
@Repository
public class StudentRepository extends BaseRepository<Student> {
@Override
public void printString(Student s) {
System.out.println("I am StudentRepo - " + s.toString());
}
}
1
2
3
4
5
6
7
@Repository
public class FacultyRepository extends BaseRepository<Faculty> {
@Override
public void printString(Faculty f) {
System.out.println("I am FacultyRepo - " + f.toString());
}
}

然后当我们调用业务层的test方法进行测试时,控制台打出了如下信息:

1
2
3
4
I am StudentRepo - Student [id=1, name=Anna, grade=3.9]
class com.hpe.bboss.autotest.dao.Student
I am FacultyRepo - Faculty [id=2, name=Bob, evaluation=A]
class com.hpe.bboss.autotest.dao.Faculty

这说明Spring为我们注入的是我们所希望的子类StudentRepository和FacultyRepository,而不是父类BaseRepository。

  1. 当@Component注解标注的多个子类同时继承一个父类,并且泛型的具体类型也相同时,按照以上方法进行依赖注入会抛出异常。这是因为@Autowired注解默认只有一个Bean与指定字段的类型相同,当拥有多个Bean满足条件的时候,就会抛出异常。这个问题的解决办法有使用@Primary注解,使用@Qualifier注解和它的子注解,使用Bean名字注入等。由于这个问题是Spring依赖注入的问题,而不是泛型依赖注入独有的,因此不再赘述,请大家查阅Spring文档和其他资料来获得具体解决办法。

  2. 泛型赖注入并不仅限于在持久层使用。我们也可以在持久层使用泛型依赖注入的基础上,在业务层等其他地方也使用泛型依赖注入。相关的例子在网上很好找到,我就不复制粘贴了,有兴趣的话请自行查阅。

两种依赖注入方式的比较

前文所讲的两种依赖注入方式,本质上是两种不同的声明Bean的方式。如前文所说,Spring对这两种声明方式都拥有很好的支持,但是这两种声明方式本身还是拥有比较大的差异。第一种方式中,我们通过@Configuration注解标注配置类来进行声明。第二种方式中,我们通过注解直接在子类进行声明。下面我就来简单探讨一下两种方式的优劣。

第一种声明方式中,所有的Bean都会在配置类中进行声明。因此在后续进行维护时,我们不需要查看每个类的源代码就可以对Bean的状态进行一些修改。另外,这种方式也意味着我们不需要为每一个Bean都创建一个子类,使得目录的管理变得简单。但是使用这种方法意味着我们每声明一个新的Bean就需要对配置类添加一个方法和至少一个注解,并且有时还需要向匿名子类中添加一些方法,在Bean数量很多时配置类的长度会变得很长,不便于理解和管理。

而第二种声明方式中,我们根本不需要维护配置文件,所有声明Bean所需要的工作,例如名字,类,加入Spring容器等,都由一个注解完成。于此同时,由于子类的存在,我们可以很方便的进行添加方法和字段,覆盖方法等工作。但是使用这种方法也意味着当我们需要对Bean的状态进行修改时,我们必须找到相应的类才能进行操作。而且大量的子类会让我们的目录更加繁杂,尤其是空子类,本身没有太大意义,却让目录的管理变得很麻烦。

综上所述,两种方式各有各的优缺点,而使用哪种方法应该根据项目的具体情况而定。一般来说,当子类中空类较多时,可能使用第一种方法比较合适,反之第二种方法比较合适。在一些难以决定的情况下,两种方法同时使用有时也是一种可以考虑的选择。但是两种方法同时使用会提高维护的难度,建议谨慎使用。

泛型依赖注入总结与展望

Spring 4.0版本新加入的泛型依赖注入功能是一个很实用的功能。它帮助我们利用泛型极大的精简了代码,降低了维护成本。根据我这次的学习和使用来看,Spring对泛型依赖注入的支持总体质量还是很不错的。泛型依赖注入的实现与普通依赖注入差别并不大,学习起来简单易懂,使用上也没有什么难度。希望看到这篇文章的大家在以后使用Spring的时候也试着试用一下泛型依赖注入。

不过,Spring4的泛型依赖注入也有一些可以改进的地方。我这次研究Spring泛型注入的初衷就是找到一种简单的注入方法,可以让我在使用Spring依赖注入的同时,尽可能的减少声明的类的数量。但是经过我这段时间的学习,我发现Spring目前必须喂每一个Bean声明一个新的类,无论是匿名子类还是空子类,否则Spring就不能正确进行依赖注入。但是当我们不需要往子类里添加任何功能时,匿名子类或者空子类过多,这个配置类就变得很低效,无论是声明还是维护管理都非常麻烦。我希望以后的Spring更新时,能够自动为我们创建这些匿名子类,或者通过一些别的方式,让我们既不需要配置类又不需要子类就可以成功的声明一些使用泛型的Bean,并且根据泛型的类型进行依赖注入。比如我希望这样声明Bean

1
2
3
4
5
6
7
8
9
10
11
12
@Repository
public class BaseRepository<T> {
public void printString(T t) {
System.out.println(t);
}

public void printType() {
Type genericSuperclass = this.getClass().getGenericSuperclass();
Class<T> entityClass = (Class<T>) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
System.out.println(entityClass);
}
}

然后在进行依赖注入的时候,Spring可以通过字段的类型来自动生成匿名子类,并进行注入:

1
2
@Autowired private BaseRepository<Student> studentRepo; //自动注入BaseRepository<Student>() {}
@Autowired private BaseRepository<Faculty> facultyRepo; //自动注入BaseRepository<Faculty>() {}

如果Spring可以做到这样的话,我相信我们的开发会变的更加高效。

感谢您的支持!