Spring 现在已经成为了 J2EE 开发的规范。只要项目中需要对 “对象” 进行管理,都得用到 Spring 这个容器框架。
Spring 有两大特性:控制反转(IOC),面向切面编程(AOP)。IOC 是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。
IOC 主要有两种实现方式:依赖注入(DI),依赖查找(DL)。Spring 则是通过依赖注入来实现的控制反转。
本场 Chat 主要介绍 Spring 常用的三种依赖注入的方式:
- 通过构造方法注入
- 通过 set 方法注入
- 基于注解的方式注入(实际工作多用这种)
Spring 通过 DI(依赖注入)实现 IOC(控制反转),常用的注入方式主要有三种:构造方法注入,set 方法注入,基于注解的注入。
一、通过构造方法注入
先简单了解一下测试项目的结构,用 maven 构建的,四个包:
- entity:存储实体,里面只有一个 User 类
- dao:数据访问,一个接口,两个实现类
- service:服务层,一个接口,一个实现类,实现类依赖于 IUserDao
- test:测试包
在 spring 的配置文件中注册 UserService,将 UserDaoJdbc 通过 constructor-arg 标签注入到 UserService 的某个有参数的构造方法
<!-- 注册 userService --><bean id="userService" class="com.lyu.spring.service.impl.UserService"> <constructor-arg ref="userDaoJdbc"></constructor-arg></bean><!-- 注册 jdbc 实现的 dao --><bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>
如果只有一个有参数的构造方法并且参数类型与注入的 bean 的类型匹配,那就会注入到该构造方法中。
public class UserService implements IUserService { private IUserDao userDao; public UserService(IUserDao userDao) { this.userDao = userDao; } public void loginUser() { userDao.loginUser(); }}
@Testpublic void testDI() { ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); // 获取 bean 对象 UserService userService = ac.getBean(UserService.class, "userService"); // 模拟用户登录 userService.loginUser();}
测试打印结果:jdbc-登录成功
注:模拟用户登录的 loginUser 方法其实只是打印了一条输出语句,jdbc 实现的类输出的是:jdbc-登录成功,mybatis 实现的类输出的是:mybatis-登录成功。
问题一:如果有多个有参数的构造方法并且每个构造方法的参数列表里面都有要注入的属性,那 userDaoJdbc 会注入到哪里呢?
public class UserService implements IUserService { private IUserDao userDao; private User user; public UserService(IUserDao userDao) { System.out.println("这是有一个参数的构造方法"); this.userDao = userDao; } public UserService(IUserDao userDao, User user) { System.out.println("这是有两个参数的构造方法"); this.userDao = userDao; this.user = user; } public void loginUser() { userDao.loginUser(); }}
结果:会注入到只有一个参数的构造方法中,并且经过测试注入哪一个构造方法与构造方法的顺序无关
问题二:如果只有一个构造方法,但是有两个参数,一个是待注入的参数,另一个是其他类型的参数,那么这次注入可以成功吗?
public class UserService implements IUserService { private IUserDao userDao; private User user; public UserService(IUserDao userDao, User user) { this.userDao = userDao; this.user = user; } public void loginUser() { userDao.loginUser(); }}
结果:失败了,即使在 costract-arg 标签里面通过 name 属性指定要注入的参数名 userDao 也会失败.
问题三:如果我们想向有多个参数的构造方法中注入值该在配置文件中怎么写呢?
public class UserService implements IUserService { private IUserDao userDao; private User user; public UserService(IUserDao userDao, User user) { this.userDao = userDao; this.user = user; } public void loginUser() { userDao.loginUser(); }}
参考写法:通过 name 属性指定要注入的值,与构造方法参数列表参数的顺序无关。
<!-- 注册 userService --><bean id="userService" class="com.lyu.spring.service.impl.UserService"> <constructor-arg name="userDao" ref="userDaoJdbc"></constructor-arg> <constructor-arg name="user" ref="user"></constructor-arg></bean><!-- 注册实体 User 类,用于测试 --><bean id="user" class="com.lyu.spring.entity.User"></bean><!-- 注册 jdbc 实现的 dao --><bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>
问题四:如果有多个构造方法,每个构造方法只有参数的顺序不同,那通过构造方法注入多个参数会注入到哪一个呢?
public class UserService implements IUserService { private IUserDao userDao; private User user; public UserService(IUserDao userDao, User user) { System.out.println("这是第二个构造方法"); this.userDao = userDao; this.user = user; } public UserService(User user, IUserDao userDao) { System.out.println("这是第一个构造方法"); this.userDao = userDao; this.user = user; } public void loginUser() { userDao.loginUser(); }}
结果:哪个构造方法在前就注入哪一个,这种情况下就与构造方法顺序有关。
二、通过 set 方法注入
配置文件如下:
<!-- 注册 userService --><bean id="userService" class="com.lyu.spring.service.impl.UserService"> <!-- 写法一 --> <!-- <property name="UserDao" ref="userDaoMyBatis"></property> --> <!-- 写法二 --> <property name="userDao" ref="userDaoMyBatis"></property></bean><!-- 注册 mybatis 实现的 dao --><bean id="userDaoMyBatis" class="com.lyu.spring.dao.impl.UserDaoMyBatis"></bean>
注:上面这两种写法都可以,spring 会将 name 值的每个单词首字母转换成大写,然后再在前面拼接上"set"构成一个方法名,然后去对应的类中查找该方法,通过反射调用,实现注入。
切记:name 属性值与类中的成员变量名以及 set 方法的参数名都无关,只与对应的 set 方法名有关,下面的这种写法是可以运行成功的
public class UserService implements IUserService { private IUserDao userDao1; public void setUserDao(IUserDao userDao1) { this.userDao1 = userDao1; } public void loginUser() { userDao1.loginUser(); }}
还有一点需要注意:如果通过 set 方法注入属性,那么 spring 会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则 spring 没有办法实例化对象,导致报错。
三、基于注解的方式注入
在介绍注解注入的方式前,先简单了解 bean 的一个属性 autowire,autowire 主要有三个属性值:constructor,byName,byType。
- constructor:通过构造方法进行自动注入,spring 会匹配与构造方法参数类型一致的 bean 进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的 bean,那么 spring 会优先将 bean 注入到多参数的构造方法中。
- byName:被注入 bean 的 id 名必须与 set 方法后半截匹配,并且 id 名称的第一个单词首字母必须小写,这一点与手动 set 注入有点不同。
- byType:查找所有的 set 方法,将符合符合参数类型的 bean 注入。
下面进入正题:注解方式注册 bean,注入依赖
主要有四种注解可以注册 bean,每种注解可以任意使用,只是语义上有所差异:
- @Component:可以用于注册所有 bean
- @Repository:主要用于注册 dao 层的 bean
- @Controller:主要用于注册控制层的 bean
- @Service:主要用于注册服务层的 bean
描述依赖关系主要有两种:
@Resource:java 的注解,默认以 byName 的方式去匹配与属性名相同的 bean 的 id,如果没有找到就会以 byType 的方式查找,如果 byType 查找到多个的话,使用@Qualifier 注解(spring 注解)指定某个具体名称的 bean。
@Resource@Qualifier("userDaoMyBatis")private IUserDao userDao;public UserService(){}
@Autowired:spring 注解,默认是以 byType 的方式去匹配类型相同的 bean,如果只匹配到一个,那么就直接注入该 bean,无论要注入的 bean 的 name 是什么;如果匹配到多个,就会调用 DefaultListableBeanFactory 的 determineAutowireCandidate 方法来决定具体注入哪个 bean。determineAutowireCandidate 方法的内容如下:
// candidateBeans 为上一步通过类型匹配到的多个 bean,该 Map 中至少有两个元素。 protected String determineAutowireCandidate(Map<String, Object> candidateBeans, DependencyDescriptor descriptor) { // requiredType 为匹配到的接口的类型 Class<?> requiredType = descriptor.getDependencyType(); // 1. 先找 Bean 上有@Primary 注解的,有则直接返回 String primaryCandidate = this.determinePrimaryCandidate(candidateBeans, requiredType); if (primaryCandidate != null) { return primaryCandidate; } else { // 2.再找 Bean 上有 @Order,@PriorityOrder 注解的,有则返回 String priorityCandidate = this.determineHighestPriorityCandidate(candidateBeans, requiredType); if (priorityCandidate != null) { return priorityCandidate; } else { Iterator var6 = candidateBeans.entrySet().iterator(); String candidateBeanName; Object beanInstance; do { if (!var6.hasNext()) { return null; } // 3. 再找 bean 的名称匹配的 Entry<String, Object> entry = (Entry)var6.next(); candidateBeanName = (String)entry.getKey(); beanInstance = entry.getValue(); } while(!this.resolvableDependencies.values().contains(beanInstance) && !this.matchesBeanName(candidateBeanName, descriptor.getDependencyName())); return candidateBeanName; } }}
determineAutowireCandidate 方法的逻辑是:
① 先找 Bean 上有@Primary 注解的,有则直接返回 bean 的 name。
② 再找 Bean 上有 @Order,@PriorityOrder 注解的,有则返回 bean 的 name。
③ 最后再以名称匹配(ByName)的方式去查找相匹配的 bean。
可以简单的理解为先以 ByType 的方式去匹配,如果匹配到了多个再以 ByName 的方式去匹配,找到了对应的 bean 就去注入,没找到就抛出异常。
还有一点要注意:如果使用了 @Qualifier 注解,那么当自动装配匹配到多个 bean 的时候就不会进入 determineAutowireCandidate 方法(亲测),而是直接查找与 @Qualifer 指定的 bean name 相同的 bean 去注入,找到了就直接注入,没有找到则抛出异常。
tips:大家如果认真思考可能会发现 ByName 的注入方式和 @Qualifier 有点类似,都是在自动装配匹配到多个 bean 的时候,指定一个具体的 bean,那它们有什么不同呢?
ByName 的方式需要遍历,@Qualifier 直接一次定位。在匹配到多个 bean 的情况下,使用 @Qualifier 来指明具体装配的 bean 效率会更高一下。
笔者个人觉得:@Qualifer 注解出现的意义或许就是 Spring 为了解决 JDK 自带的 ByName 遍历匹配效率低下的问题。要不然也不会出现两个容易混淆的匹配方式。
四、总结
虽然有这么多的注入方式,但是实际上开发的时候自己编写的类一般用注解的方式注册类,用@Autowired 描述依赖进行注入,一般实现类也只有一种(jdbc or hibernate or mybatis),除非项目有大的变动,所以@Qualifier 标签用的也较少;但是在使用其他组件的 API 的时候用的是通过 xml 配置文件来注册类,描述依赖,因为你不能去改人家源码嘛。
当然,现在很多公司包括笔者所在的公司都已经开始使用 SpringBoot 来搭建项目了,通过 xml 配置的方式来注入对象的时代也已经一去不复返。在面试的时候,面试官可能会问一下你这种偏理论的问题;但实际开发 80% 都是使用注解的方式。毕竟,你的老板要的是结果,能够快速高效的完成老板产品提出的需求才是王道,他们才不管你到底用的是什么方式注入对象,反正明天给我上线就行...
用于测试的项目的网盘链接如下:https://pan.baidu.com/s/1GrRlT5cLAI3SMu17TA6l6Q
阅读全文: http://gitbook.cn/gitchat/activity/5e08242b380c1173500f85da
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。