依赖注入
- 从一个例子说起
- 控制反转
- 依赖注入
- 构造器注入
- setter注入
- 依赖注入的本质
从一个例子说起
我们看看Martin Fowler的一个例子。
我们有一个MoiveLister
class:
class MovieLister...
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}
它有一个功能,就是给出导演名字,就能够找到该导演所有的影片。
(我给出Movie类的代码:
package com.ocean.spring.pre;
public class Movie {
private String title;
private String director;
private String actor;
String getTitle() {
return title;
}
void setTitle(String title) {
this.title = title;
}
String getDirector() {
return director;
}
void setDirector(String director) {
this.director = director;
}
String getActor() {
return actor;
}
void setActor(String actor) {
this.actor = actor;
}
@Override
public String toString() {
return "Movie{" +
"title='" + title + '\'' +
", director='" + director + '\'' +
", actor='" + actor + '\'' +
'}';
}
}
)
为此,它需要一个finder
来告诉它所有的电影。
我们知道,组件与组件之间是通过接口进行交流的,所以我们定义:
public interface MovieFinder {
List findAll();
}
接口能够定义功能,但是无法具体地工作。
所以我们不得不给出实现类:
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movie1.txt");
}
package com.ocean.spring.pre;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
public class ColonDelimitedMovieFinder implements MovieFinder {
private String filename;
public ColonDelimitedMovieFinder(String filename) {
this.filename = filename;
}
@Override
public List findAll() {
List movies = new ArrayList();
File file = new File("src/main/resources/" + filename);
try {
Scanner scanner = new Scanner(file);
Map<String, String> movieData = new HashMap<>();
while (scanner.hasNext()) {
String next = scanner.nextLine();
if (!"".equals(next.trim())) {
String[] data = next.split(": ");
if ("title".equals(data[0])) {
movieData.put("title", data[1]);
}
if ("director".equals(data[0])) {
movieData.put("director", data[1]);
}
if ("actor".equals(data[0])) {
movieData.put("actor", data[1]);
}
}
else {
Movie movie = new Movie();
movie.setTitle(movieData.get("title"));
movie.setDirector(movieData.get("director"));
movie.setActor(movieData.get("actor"));
movies.add(movie);
}
}
}
catch (Exception e) {
}
return movies;
}
}
比如我的movie1.txt
是这么写的:
title: dragon
director: ocean
actor: lin
title: peace
director: ocean
actor: peter
title: nobody
director: sting
actor: bob
---------------------
这当然毫无问题。
突然有一天,我朋友要了这份代码的拷贝。如果他的movie格式也是我这样的用冒号分隔开的,一切工作如故。
可是,如果他的电影使用xml存储的呢,是用json存储的呢?
这样我们就要写一个XMLMovieFinder
,写个JsonMovieFinder
……
根据上图,MovieLister
依赖了MovieFinder
接口以及该接口的实现类。
如果我们只让MovieLister
依赖MovieFinder
接口、而非它的实现类行不行呢?
也就是说,在编译期,MovieLister
只与MovieFinder
合作,因为我根本不知道我的朋友用的是什么文件格式。只有到了运行期,才能动态地知道究竟是哪个具体的实现类在工作。
Martin Fowler将实现类比作一种插件,我们的lister既要不知道它,又要和它合作。
我们该怎么做呢?
(为什么这个问题这么重要?因为项目扩大,成百上千的组件,我们希望用不同的方式部署,因此我们一定要找到使不同组件沟通的办法)
控制反转
学过spring的朋友会说,spring的特点是控制反转。但是Martin Fowler说,说控制反转是特点就好像说车子的特点是有四个轮子一样。
关键是:
what aspect of control are they inverting?
它们反转的是什么层面的控制?
前面的例子中,反转指的是如何找到插件。例子中使用的是直接在构造方法里实例化了。但我们希望插件是一个额外的模块注入到lister中。
因此,我们把这种行为叫做依赖注入。
(控制反转太泛泛而谈了,所以Martin Fowler改了名字。)
依赖注入
依赖注入的核心是:我们需要一个分开的组件,这个东西能够在lister中注入字段,而这个字段就是MovieFinder
接口的某个实现类。
依赖注入的三种方式:构造器注入、setter注入、接口注入。接口注入会使项目变得庞大,我们只使用前两种。
构造器注入
我们只需使用@Autowired
注解即可(spring4.3后可以省略)。
@Autowired
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
如此便可将MovieFinder
的实现类告知给MovieLister
。
当然我们也要给确定的实现类注入值:
public ColonDelimitedMovieFinder(@Value("movie1.txt") String filename) {
this.filename = filename;
}
为了使得这两个类能够被容器管理,我们需要加上@Component
注解。
@Component
public class MovieLister
@Component
public class ColonDelimitedMovieFinder implements MovieFinder
如果MovieFinder
有多个实现类,只用将我们需要的那个加上@Component
注解即可。
最后,我们要让spring能够扫到这些注解:
@Configuration
@ComponentScan(value = "com.ocean.spring.pre")
public class MovieConfig {
}
测试:
public class MovieTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(MovieConfig.class);
MovieLister movieLister =
(MovieLister) context.getBean("movieLister");
Movie[] oceans = movieLister.moviesDirectedBy("ocean");
Stream.of(oceans).forEach(System.out::println);
}
}
现在,所有东西都是从AnnotationConfigApplicationContext
这么一个东西里拿出来的。
setter注入
为了防止构造器注入带来的干扰,我们将构造器注入代码删掉,不再提供有参构造。
并且使用
@Autowired
void setFinder(MovieFinder finder) {
this.finder = finder;
}
为MovieLister
注入一个finder
。
这就完成了。
依赖注入的本质
以上的两种注入,更确切的说,应该是基于构造器的注入和基于setter的注入。
为什么要加基于呢?
我们知道,@Autowired
还可以用在字段上来为其注入值,难道这又是一种新的注入方式吗?
我们在谈依赖注入的时候,谈的是如何将A注入B。
要使A注入B,只能是通过B的构造方法将A传入,或者通过setA(A a)
的方式。
因此,本质上只有这两种方式。
那么字段是如何注入值的呢?
比如我有一个UserController
,它依赖于UserService
:
public class UserController {
@Autowired
private UserService userService;
public UserService getUserService() {
return userService;
}
}
这里的@Autowired
是我们自己写的:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}
@Test
public void testAutowired(){
UserController userController = new UserController();
Class<? extends UserController> aClass = userController.getClass();
Stream.of(aClass.getDeclaredFields()).forEach(field -> {
Autowired annotation = field.getAnnotation(Autowired.class);
if(annotation!=null){
field.setAccessible(true);
Class<?> type = field.getType();
try {
//spring中这个object不可能直接new出来
Object newInstance = type.getConstructor().newInstance();
//setter注入
field.set(userController,newInstance);
} catch (Exception e){
e.printStackTrace();
}
}
});
System.out.println(userController.getUserService());
}
能够在运行期动态地做某些事情,要么通过反射,要么通过动态代理。
以上是用反射拿到@Autowired
注解的字段。
然后拿到字段类型并实例化。
然后用 field.set()
将UserService
的实例注入到UserController
中。