前言

用过Spring系列的框架的同学们,肯定对@Autowired注解并不陌生,我们可以通过@Autowired来对成员变量、方法以及构造函数进行修饰,使Spring对其完成Bean的自动装配功能。
再学校期间,我一直都是将@Autowired来修饰要使用的变量(毕竟代码量少),例如:

@Autowired
UserService userService;

在最近,我发现公司的项目都不用@Autowired来修饰成员变量,而是用来修饰构造函数,在构造函数中再去对相应的bean进行赋值,例如:

UserService userService;
@Autowired
UserController(UserService userService){
    this.userService = userService;
}

那这两种方案,都有什么区别吗?为什么公司要选择代码量较多的方案呢?

区别

首先,@Autowired注解都是为了让Spring去完成自动装配的功能,其功能上是没有区别的。因此,我们都可以用两种方法来实现自动装配:
在此案例中,我们使用两种不同的方式,在对UserService进行装配,并验证其功能

@Slf4j
@Service
public class UserService {
    UserService(){
        log.info("userService自动装配成功");
    }

    public void test(){
        log.info("调用test方法成功");
    }
}

参数装配

@RestController
@RequestMapping(value = "/user/")
public class UserController {
    @Autowired
    UserService userService;

    @GetMapping(value = "/test")
    public String test(){
        userService.test();
        return "success";
    }
}

我们可以看到,Spring对UserService完成了自动装配,我们访问相应的路径,可以看到成功的调用了test()方法:
image.png

构造函数装配

@RestController
@RequestMapping(value = "/user/")
public class UserController {
    UserService userService;

    @Autowired
    UserController(UserService userService){
        this.userService = userService;
    }
    
    @GetMapping(value = "/test")
    public String test(){
        userService.test();
        return "success";
    }
}

我们可以看到,Spring也是正常完成了UserService的装配,访问对应页面,也是成功调用了相应方法:
image.png

通过上面实验我们可以看到,不管是通过修饰参数,还是通过修饰构造方法,都能够完成自动注入,其功能也能够正常访问。
那么,这两者的区别是什么呢?了解Java类加载顺序的同学可能明白了,或许,会和加载和注入顺序有关。

Java类加载和注入顺序

在上面的代码中,类加载的顺序应该是这样的:
静态变量或静态语句块–>实例变量或初始化语句块–>构造方法–>@Autowired
因此,注入通常是在最后执行的,但如果我们将注解修饰在构造方法上,那注入操作就会在实例对象时执行。
也就是说,如果我们将注解修饰在构造方法上,那么我们的自动装配时机就不会排在最后一步。那么,在实际生产环境中,又有什么区别吗?
我们来看看下面情况,我们使用两种方法来进行自动装配。

@Slf4j
@Service
public class UserService {
    UserService(){
        log.info("userService自动装配成功");
    }

    public String getNane(){
        return "刘欣";
    }
}

使用构造方法

@RestController
@RequestMapping(value = "/user/")
public class UserController {
    UserService userService;
    String username;
    @Autowired
    UserController(UserService userService){
        this.userService = userService;
        this.username = userService.getNane();
    }

    @GetMapping(value = "/test")
    public String test(){
        return username;
    }
}

运行代码,我们可以看到,程序正常运行
image.png
访问页面,也能正常获取数据:
image.png

使用参数

@RestController
@RequestMapping(value = "/user/")
public class UserController {
    @Autowired
    UserService userService;
    String username = userService.getNane();

 

    @GetMapping(value = "/test")
    public String test(){
        return username;
    }
}

运行代码,我们发现在启动SpringBoot应用程序时,便抛出了异常:
image.png

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userController' defined in file [.../UserController.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [.../UserController]: Constructor threw exception; nested exception is java.lang.NullPointerException

异常告诉我们,因为实例化bean失败,而导致了空指针异常。
那么这是为什么呢?与之前的代码不同的是,我们在参数中直接调用了getName方法,而在该程序中,加载顺序应该是这样的:
定义UserService变量 - 定义userName变量 - 调用getName方法 - 构造方法 - 实例化Bean。
我们看到,在UserService还未实例化时,我们就调用了getName方法,因此导致了NullPointerException。

总结

在开发过程中,如果有使用到bean的实例变量的定义,应使用构造方法的形式去实现自动注入。
我们在实际进行开发过程中,应该去考虑多种方案的优缺点以及区别,而不是一味的去寻求所谓有简便化开发,去追寻所谓的效率。可能,在我们追寻所谓的效率时,出现问题后,会更花费我们的精力。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议