Spring——@Autowired注解以及与@Resource区别

本篇介绍Spring中注入bean的方式之一@Autowired注解的使用方法;同为注入bean的注解@Resource,比较@Autowired与其在使用上的区别及注意事项;这部分知识也是面试经常会问的知识点,需要掌握;

1. @Autowired注解介绍及各个使用场景示例

先看看@Autowired注解的定义:

// org.springframework.beans.factory.annotation.Autowired

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

	/**
	 * Declares whether the annotated dependency is required.
	 * <p>Defaults to {@code true}.
	 */
	boolean required() default true;

}

可以看出该注解能够使用在5种目标类型上:

  • 构造器
  • 方法
  • 方法参数
  • 成员属性
  • 注解

我们自己开发过程中,用的最多是Field成员属性上;接下来,我们重点看看在其他地方该怎么用;

1.1 成员变量

在成员变量上使用Autowired注解,这种方式可能是平时用得最多的;

@Service
public class ServiceAImpl implement ServiceA {

    @Autowired
    private ServiceB serviceB;
}

1.2 构造器

在构造器上使用Autowired注解,实际上还是使用了Autowired装配方式,并非构造器装配;

@Service
public class ServiceAImpl implement ServiceA {

    private ServiceB serviceB;

    @Autowired
    public ServiceAImpl(ServiceB serviceB) {
        this.serviceB = serviceB;
        System.out.println("serviceB:" + serviceB);
    }
}

1.3 方法

可以在普通方法上加Autowired注解,spring会在项目启动的过程中,自动调用一次加了@Autowired注解的方法,我们可以在该方法做一些初始化的工作,这种写法一般用于框架中;

@Service
public class ServiceA {

//    @Autowired
//    private ServiceB serviceB;

    @Autowired
    public void init(ServiceB serviceB){
        final String init4ServiceA = serviceB.init4ServiceA();
        log.info(init4ServiceA);
    }
}

注意,这种写法一般是要求被加Autowired注解的方法包含入参的,且参数类型的bean是存在的;如果不加参数,也能启动成功但是打印提示"Autowired annotation should only be used on methods with parameters";如果方法参数对应的bean不存在,也会有提示"Could not autowire. No beans of 'Xx' type found.",这种情况下编译器提示error,是无法启动的;

除了普通方法,当然也可以在setter方法上Autowired注解,该方式与属性注入一样;

@Service
public class ServiceA {

    private ServiceB serviceb;

    @Autowired
    public void setUser(ServiceB serviceb) {
        this.serviceb= serviceb;
    }
}

1.4 方法参数

可以在构造器的入参上加Autowired注解;

@Service
public class ServiceA {

    private ServiceB serviceB;

    public ServiceA(@Autowired ServiceB serviceB) {
        this.serviceB= serviceB;
    }
}

也可以在非构造器方法的普通方法入参上加Autowired注解;

@Service
public class ServiceA {

    public void test(@Autowired ServiceB serviceB) {
        serviceB.doB();
    }
}

1.5 注解

这种方式其实用得不多,就不介绍了;

2.  使用@Autowired时可能遇到的问题

2.1 自动装配的bean不存在

依赖倒置原则DIP是Spring IOC的设计原理,依赖注入DI是IOC的实现方式;

这一思想通俗的讲就是:流程固定的条件下,我们有什么实现,程序执行就用什么;这也是依赖倒置原则(DIP)所表达的——程序不应依赖于实现,而应依赖于抽象;

这种设计思想在一些框架内,包括Spring内部都有类似的实现;例如这样的一个场景:某个异步执行器代理,在准备异步执行方法时,先去IOC容器中去找一个类型为"TaskExecutor"且名字叫"taskExecutor"的bean,作为异步执行的线程池;如果找到了则用这个bean,如果没找到则使用自己事先根据默认参数创建的线程池;

在对这个异步执行代理bean执行依赖注入的过程中,Spring会去扫描这个名为"taskExecutor"的bean;例如下面的例子,要在MyService中注入BaseService,如果直接使用@Autowired注解注入(注解的属性的默认值是true),若这个名为"baseService"的bean未找到,注入就会失败,程序提示"找不到当前类型的bean";如下:

启动时会报错:

***************************
APPLICATION FAILED TO START
***************************

Description:
Field baseService in com.internet.demo.service.testautowire.MyService required a bean of type 'com.internet.demo.service.testautowire.BaseService' that could not be found.

Action:
Consider defining a bean of type 'com.internet.demo.service.testautowire.BaseService' in your configuration.

而我们期望的结果是:有bean就注入,没有找到bean就不注入,改为使用默认线程池;因此解决办法就是改为使用@Autowired(required=false),表示我们不使用自动装配功能,即忽略当前要注入的bean,如果有则直接注入,如果没有则跳过,不会启动时报错;如下,代码不再提示错误,启动成功;

2.2 同类型的多个bean的注入失败

Spring中@Autowired注解是按照bean的类型装配的,也就是我们所说的byType方式;当相同type的bean存在多个时,我们在一个bean中注入这个type的bean,就会报错;

先尝试一种操作,在项目的不同包目录下,分别建了一个同名的类ServiceA,然后启动工程,发现报错:

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'serviceA' for bean class [com.internet.demo.service.testautowire.ServiceA] conflicts with existing, non-compatible bean definition of same name and class [com.internet.demo.service.cycledependence.ServiceA]

注意,这个错误不管是否有没有在其他bean中注入ServiceA,他都会报错;其原因并不是由于ServiceA的type下存在多个bean,而是因为Spring的@Service注解下不允许出现相同的类名,因为Spring会将类名的第一个字母转换成小写,作为bean的名称,如:serviceA,而默认情况下bean名称必须是唯一的;这一点非常容易产生混淆,需要注意;

下面演示一种产生两个相同的类型bean的情况;定义接口BaseService,让A和B分别实现,然后将类型的bean注入;

@Service
public class BaseServiceImplA implements BaseService {
    @Override
    public void baseMethod() {}
}

@Service
public class BaseServiceImplB implements BaseService {
    @Override
    public void baseMethod() {}
}

其实编译器已经会提示我们,同类型下出现了多个bean,启动会报错;

***************************
APPLICATION FAILED TO START
***************************

Description:
Field baseService in com.internet.demo.service.testautowire.MyService required a single bean, but 2 were found:
	- baseServiceImplA: defined in file [C:\IDE_Apps\demo\target\classes\com\internet\demo\service\testautowire\BaseServiceImplA.class]
	- baseServiceImplB: defined in file [C:\IDE_Apps\demo\target\classes\com\internet\demo\service\testautowire\BaseServiceImplB.class]

Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

还有一个情况会产生两个相同的类型bean,如定义了一个配置类,在其中手动注册多个同类型但不同名字的bean,也会报错,如下;

@Configuration
public class BeanConfig {

    @Bean
    public BaseService getBaseServiceA(){
        return new BaseServiceImplA();
    }

    @Bean
    public BaseService getBaseServiceB(){
        return new BaseServiceImplB();
    }

}

可以看到报错的提示里面已经提示了解决思路,出现了2个注解:@Primary和@Qualifier;显然在spring中,按照Autowired默认的装配方式:byType,是无法解决上面的问题的,这时可以改用按名称装配:byName,而@Qualifier注解就有这个作用;

在注入BaseService的地方,在@Autowired注解上面,同时加上@Qualifier注解即可,它指定一个bean的名称,通过bean名称找到需要装配的bean;

@Service
public class MyService {

    @Autowired
    @Qualifier("baseServiceImplA")
    private BaseService baseService;

}

另一个注解@Primary注解也可以解决上面的问题,如在BaseService的其中一个实现类上面加上@Primary注解,当注入BaseService类型的bean存在多个时,被@Primary标记的bean会作为主bean被注入;

@Primary
@Service
public class BaseServiceImplA implements BaseService {
    @Override
    public void baseMethod() {}
}

2.3 用集合去接收多个同类型的bean

上面举的例子都是通过@Autowired自动装配单个实例,实际上它也能自动装配多个实例,这是一个比较巧妙的用法,并且@Resource注解也支持;

@Slf4j
@Service
public class MyService {

    @Autowired
    private List<BaseService> baseServiceList;
    @Autowired
    private Map<String, BaseService> baseServiceMap;

    public void printDependence() {
        log.info("baseServiceList={}", baseServiceList);
        log.info("baseServiceMap={}", baseServiceMap);
    }
}

打印分别打印集合,发现多个bean都被放入了集合;对于Map类型,key存放的就是bean的name,对于Config形式注册的bean,name就是打了@Bean注解的方法名;

baseServiceList=[
com.internet.demo.service.testautowire.BaseServiceImplA@511f5b1d, 
com.internet.demo.service.testautowire.BaseServiceImplB@31f295b6, 
com.internet.demo.service.testautowire.BaseServiceImplA@1ba467c2, 
com.internet.demo.service.testautowire.BaseServiceImplB@63f6bed1]

baseServiceMap={
baseServiceImplA=com.internet.demo.service.testautowire.BaseServiceImplA@511f5b1d, 
baseServiceImplB=com.internet.demo.service.testautowire.BaseServiceImplB@31f295b6, 
getBaseServiceA=com.internet.demo.service.testautowire.BaseServiceImplA@1ba467c2, 
getBaseServiceB=com.internet.demo.service.testautowire.BaseServiceImplB@63f6bed1}

这种写法往往在使用模板方法设计模式时,根据type取出对应的模板Service时会用到;

2.4 当前类需要先注册为bean

在使用@Autowired注入bean的类上,该类忘了加@Controller、@Service、@Component、@Repository等注解,Spring就无法完成自动装配的功能,这种情况应该是小白最常见的错误了,虽然是低级的错误,但很多人包括本人校招入职刚写代码时都干过~

另外,bean未被scan扫描也是常见的问题,通常情况下,标识为bean的@Controller、@Service、@Component等注解,是需要被扫描的;但是,如果注解扫描的路径配的不对,或者路径范围太小,会导致有些类未被扫描到并注册为bean,导致无法使用@Autowired完成自动装配的功能;如springboot项目中使用@SpringBootApplication注解,可以通过注解的scanBasePackages属性配置bean扫描路径;

@SpringBootApplication(scanBasePackages = "com.internet.demo.*")
public class DemoApplication extends SpringBootServletInitializer {

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

2.5 注入Filter或Listener时失败

这个点很重要!在开发中,需要自定义一个Filter,并且在里面注入一个Service时,会发现tomcat启动不起来;配置Filter如下:

import javax.servlet.*;
import javax.servlet.FilterConfig;
import java.io.IOException;

public class UserFilter implements Filter {
	@Autowired
    private UserService userService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        userService.initUserFilter();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //... }

    @Override
    public void destroy() {}
}
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new UserFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

启动后报错NPE:

2022-06-28 10:47:05.054 ERROR [] [localhost-startStop-1] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] 182: Exception starting filter [userFilter]

java.lang.NullPointerException: null
	at com.internet.demo.service.testautowire.UserFilter.init(UserFilter.java:29) ~[classes/:na]

分析下过程,我们定义了一个UserFilter类,将其通过Config方式注册成bean,并且在UserFilter中注入了UserService的bean用来执行Filter的初始化,结果是注入的userService对象为null,执行init方法时报了空指针;接下来分析下原因;

众所周知,web应用启动的顺序是:listener->filter->servlet;

SpringMVC的启动是在DisptachServlet里面做的,而它是在listener和filter之后执行;如果我们想在listener和filter里面@Autowired某个bean,肯定是不行的,因为filter初始化的时候,此时bean还没有被加载,所以无法自动装配,导致Tomcat无法正常启动;

实际编码中,如果真的需要在Filter中注入Service来协助执行doFilter逻辑,那有没有办法呢?

——方法有!使用WebApplicationContextUtils.getWebApplicationContext获取当前的ApplicationContext,再通过它根据beanName获取到bean实例;如下:

@Slf4j
public class UserFilter implements Filter {

    private UserService userService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 手动执行getBean 从applicationContext中取
        ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
        this.userService = ((UserService) (applicationContext.getBean("userService")));

        userService.initUserFilter();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("UserFilter.doFilter");
        // ...
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

至此,tomcat启动成功,Filter#init方法中成功的调用了UserService的bean实例的方法;

2.6 出现循环依赖

Spring的bean默认是单例的,如果单例bean使用@Autowired自动装配,大多数情况,能解决循环依赖问题;

但是如果bean是多例的,Spring解决不了此类循环依赖问题,会导致bean自动装配不了;

引入AOP下,需要创建原始bean的代理对象,即使bean是单例的,依然可能会出现循环依赖问题;

关于循环引用,我的上一篇文章《Spring循环依赖&三级缓存【建议收藏】》讲的非常细,个人感觉值得一看;

3. @Autowired与@Resource的区别

@Autowired功能虽说非常强大,但是也有些不足之处;比如:比如它是Spring框架下的注解,跟Spring框架强耦合了,如果换成了非Spring的其他框架,功能就会失效;而@Resource是javax包下的,是JSR-250提供的标准,绝大部分框架都支持;

日常很多使用@Autowired的场景,使用@Resource几乎能平替;接下来,重点看看@Autowired和@Resource的区别;

3.1 @Autowired与@Resource的区别

@Autowired@Resource
装配方式默认按byType自动装配默认byName自动装配
注解参数注解属性只包含1个参数:required,默认是true,表示是否开启自动注入注解属性有7个参数,其中最重要的两个参数是:name和type
使用方式如果要使用byName,需要使用@Qualifier一起配合如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配;也可以同时使用
使用范围能够用在:构造器、方法、参数、成员变量和注解上能用在:类、成员变量和方法上
注解来源@Autowired是Spring框架下定义的注解@Resource是JSR-250定义的注解

接下来主要理一下二者在装配bean上的逻辑;

3.2 @Autowired的装配顺序

3.3 @Resource的装配顺序

@Service
public class MyService {

    @Resource(type = BaseService.class, name = "baseServiceImplB")
    private BaseService baseService;
}

参考:

@Autowired和@Resource的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值