目 录
实现基本的 Spring 读取和存储对象的操作后,发现读取和存储对象并没有想象中的那么 “简单” ,在 Spring 中想要更简单的存储和读取对象的核心是使用注解
一.存储 Bean 对象
1.1 前置工作:配置扫描路径(重要)
- 创建并初始化Spring项目(前篇文章详解了)
- 创建 Spring 配置文件并设置 Bean 扫描的根路径
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:content="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<content:component-scan base-package="com.redamancy.study"></content:component-scan>
</beans>
注意:即使添加了注解,如果不是在配置的扫描包下的类对象,也是不能被存储到 Spring 中的。
1.2 添加注解存储 Bean 对象
想要将对象存储在 Spring 中,有两种注解类型可以实现:
- 类注解:@Controller、@Service、@Repository、@Component、@Configuration。
- 方法注解:@Bean。
添加类注解
-
@Controller:(控制器)验证前端传递的参数的【安全检查】
-
@Service:(服务层)服务调用的编排和汇总
-
@Repository:(仓库(数据仓库…))直接操作数据库
-
@Component:(组件)通用化的工具类
-
@Configuration:(配置)项目的所有配置
示例都是一样的,此处就展示一个类注解:
1.3 为什么要这么多类注解?
既然功能是⼀样的,为什么需要这么多的类注解呢?
这和为什么每个省/市都有自己的车牌号是⼀样的?
比如湖北的车牌号就是:鄂X:XXXXXX,北京的车牌号:京X:XXXXXX,一样。甚至⼀个省不同的县区也是不同的,武汉是鄂A xxx…
那么为什么需要怎么多的类注解也是相同的原因,就是让程序员看到类注解之后,就能直接了解当前类的用途,比如:
- @Controller:表示的是业务逻辑层;
- @Servie:服务层;
- @Repository:持久层;
- @Configuration:配置层;
程序的工程分层,调⽤流程如下:
还有要注意我们的配置存储 Bean 对象的扫描根路径可以和 bean 一起使用:
1.3.1 类注解之间的关系
通过源码我们可以看到:
@Controller、@Service、@Repository、@Configuration 都是基于 @Component,说明它们本身就是属于 @Component 的 “子类”,它们的作用都是将 Bean 存储到 Spring 中。
1.3.2 Bean 命名规则
默认情况下,使用 5 大类注解的 Bean 名称是将类首字母小写的命名规则,ex: UserController -> userController。
但是也有特殊情况:
例如这里前面的字母有两个大写,我们按照上面的默认规则来运行
发现这里会报错
于是查看了一下 BeanName 的源码,
它使用的是 JDK Introspector 中的 decapitalize 方法,源码如下:
也就是说,名字为空的情况下直接返回原类名,名字但凡长度大于一,我们就得看第一个和第二个字母是不是都是大写,那么此时也返回原类名,要不然就返回首字母小写,此时我们就彻底懂了它的命名规则,然后此时我们也也调用 jdk 里的这个方法来验证一下
这里对五大类注解 bean 名称规则总结一下:
1.4 方法注解 @Bean
类注解是添加到某个类上的,而方法注解是放到某个方法上的,如以下代码的实现:
public class Users {
@Bean
public User user1() {
User user = new User();
user.setId(1);
user.setName("Java");
return user;
}
}
然而,当我们写完以上代码,尝试获取 bean 对象中的 user1 时却发现,根本获取不到:
public class Application {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
User user = context.getBean("user1",Users.class);
System.out.println(user.toString());
}
}
以上程序的执⾏结果如下:
1.4.1 方法注解要配合类注解使用
在 Spring 框架的设计中,方法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中
@Component
public class Users {
@Bean
public User user1() {
User user = new User();
user.setId(1);
user.setName("Java");
return user;
}
}
再次执行以上代码,运行结果如下:
注意方法注解 bean 命名方式是方法名,而不是小驼峰类名
1.4.2 重命名 Bean
当出现了不同的类中有相同的方法名之后,都使用 五大类注解+bean注解,就会出现问题了,总会有执行不到的,那么此时就需要重命名 bean 注解,来区分他们,更好的执行到代码。
方法一:
方法二:
方法三:
方法三可以让方法注解 bean 有多个名字可使用
@Bean 重命名之后,使用方法名就不能获取Bean对象了
二.获取 Bean 对象(对象装配)
IoC 和 DI 是 Spring 中最重要的两个概念,其中 IoC(Inversion of Control)为控制反转的思想,而 DI(Dependency Injection)依赖注入为其(IoC)具体实现。那么 DI 实现依赖注入的方式有几种?这些注入方式又有什么不同?接下来,我们一起来看。
获取 bean 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注入。
在 Spring 中实现依赖注入的常见方式有以下 3 种:
- 属性注入(Field Injection);
- Setter 注入(Setter Injection);
- 构造方法注入(Constructor Injection)。
2.1 属性注入
属性注⼊是使用 @Autowired 实现的,将 Service 类注⼊到 Controller 类中。
Service 类的实现代码如下:
@Service
public class UserService {
/**
* 根据 ID 获取⽤户数据
* @param id
* @return
*/
public User getUser(Integer id) {
// 伪代码,不连接数据库
User user = new User();
user.setId(id);
user.setName("Java-" + id);
return user;
}
}
Controller 类的实现代码如下:
@Controller
public class UserController {
// 注⼊⽅法1:属性注⼊
@Autowired
private UserService userService;
public User getUser(Integer id) {
return userService.getUser(id);
}
}
获取 Controller 中的 getUser 方法:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class UserControllerTest {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController userController = context.getBean(UserController.class);
System.out.println(userController.getUser(1).toString());
}
}
属性注入的核心实现如下:
还需知道 这个方法不能在 main 方法里直接调用,因为 main 是由 static 修饰的,执行的比较早,而 @Autowired 执行时机比较晚
优缺点分析
然而,属性注入虽然使用简单,但也存在着很多问题,甚至编译器 Idea 都会提醒你“不建议使用此注入方式”
属性注入的缺点主要包含以下 3 个:
- 功能性问题:无法注入一个不可变的对象(final 修饰的对象);
- 通用性问题:只能适应于 IoC 容器;
- 设计原则问题:更容易违背单一设计原则。
缺点1:功能性问题
使用属性注入无法注入一个不可变的对象(final 修饰的对象),如下图所示:
原因也很简单:在 Java 中 final 对象(不可变)要么直接赋值,要么在构造方法中赋值,所以当使用属性注入 final 对象时,它不符合 Java 中 final 的使用规范,所以就不能注入成功了。
如果要注入一个不可变的对象,要怎么实现呢?使用下面的构造方法注入即可。
缺点2:通用性问题
使用属性注入的方式只适用于 IoC 框架(容器),如果将属性注入的代码移植到其他非 IoC 的框架中,那么代码就无效了,所以属性注入的通用性不是很好。
缺点3:设计原则问题
使用属性注入的方式,因为使用起来很简单,所以开发者很容易在一个类中同时注入多个对象,而这些对象的注入是否有必要?是否符合程序设计中的单一职责原则?就变成了一个问题。但可以肯定的是,注入实现越简单,那么滥用它的概率也越大,所以出现违背单一职责原则的概率也越大。注意:这里强调的是违背设计原则(单一职责)的可能性,而不是一定会违背设计原则,二者有着本质的区别。
2.2 Setter 注入
优缺点分析
优点:符合单一设计原则(一个Setter只针对一个对象)
缺点:
- 不能注入不可变对象(final 修饰的对象);
- 注入的对象可被修改。
- 不能注入不可变对象(final 修饰的对象)
- 注入对象可被修改
Setter 注入提供了 setXXX 的方法,意味着你可以在任何时候、在任何地方,通过调用 setXXX 的方法来改变注入对象,所以 Setter 注入的问题是,被注入的对象可能随时被修改。
2.3 构造方法注入
用法:
区别:当当前类只有一个构造方法时,@Autowired 可以省略。
注意:
- 可以在一个构造方法里注入多个对象,并且只有一个构造方法的时候,@Autowired 可以省略。
- 注入多个对象,写多个构造方法,程序都会执行吗?当有多个构造方法的时候,加了 @Autowired 的构造方法才会执行,并且构造方法中的参数(对象)必须要存在于 Spring 容器中,否则就会报错。
- 在Spring 中,一个类的构造方法可以有多个,但是只能有一个构造方法上添加 @Autowired 注解,否则会报错。
优缺点分析
构造方法注入相比于前两种注入方法,它可以注入不可变对象,并且它只会执行一次,也不存在像 Setter 注入那样,被注入的对象随时被修改的情况,它的优点有以下 4 个:
- 可注入不可变对象(可以注入 final 修饰的对象);
- 注入对象不会被修改;
- 注入对象会被完全初始化;
- 通用性更好。
优点1:可以注入 final 修饰的对象
原因:遵循Java 的规范。
final 用法只有2种:
- 创建时直接赋值
- 在构造方法中赋值
优点2:注入对象不会被修改
构造方法注入不会像 Setter 注入那样,构造方法在对象创建时只会执行一次,因此它不存在注入对象被随时(调用)修改的情况。
优点3:完全初始化
因为依赖对象是在构造方法中执行的,而构造方法是在对象创建之初执行的,因此被注入的对象在使用之前,会被完全初始化,这也是构造方法注入的优点之一。
优点4:通用性更好
通用性更好,因为构造方法是Java (JDK)支持【最底层的框架】,所以更换任何的框架,它都是适用的。
Spring 4.2之前推荐的注入用法是Setter,Setter更加符合单一设计原则。
Spring 4.2之后推荐使用构造方法注入的方式,如果你的写的代码传递了太多参数,那么就要考虑你的代码是否符合单一设计原则了。
2.4 三种注入优缺点分析
-
属性注入的优点是简洁,使用方便;缺点是只能用于 IoC 容器,如果是非 IoC 容器不可用,并且只有在使用的时候才会出现 NPE(空指针异常)。
-
Setter 方式是 Spring 前期版本推荐的注入方式,但通用性不如构造方法,所有 Spring 现版本已经推荐使用构造方法注入的方式来进行类注入了。
-
构造方法注入是 Spring 推荐的注入方式,它的缺点是如果有多个注入会显得比较臃肿,但出现这种情况你应该考虑⼀下当前类是否符合程序的单一职责的设计模式了,它的优点是通用性,在使用之前⼀定能把保证注入的类不为空。
总结
依赖注入的常见实现方式有 3 种:属性注入、Setter 注入和构造方法注入。其中属性注入的写法最简单,所以日常项目中使用的频率最高,但它的通用性不好;而 Spring 官方推荐的是构造方法注入,它可以注入不可变对象,其通用性也更好,如果是注入可变对象,那么可以考虑使用 Setter 注入。
在 @Autowired 中会有同⼀类型多个 Bean 报错处理:
解决同⼀个类型,多个 bean 的解决方案有以下两个:
- 使用 @Resource(name=“user1”) 定义。
- 使用 @Qualifier 注解定义名称。
2.5 @Resource:另⼀种注入关键字
方案一:
在进行类注入时,除了可以使用 @Autowired 关键字之外,我们还可以使用 @Resource 进行注入
方案二:
2.6 @Autowired 和 @Resource 的区别
- 来源不同
- 依赖查找的顺序不同
- 支持的参数不同
- 依赖注入的用法不同
- 来源不同
@Autowired 和 @Resource 来自不同的“父类”,其中 @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解
- 依赖查找顺序不同
依赖注入的功能,是通过先在 Spring IoC 容器中查找对象,再将对象注入引入到当前类中。而查找有分为两种实现:按名称(byName)查找或按类型(byType)查找,其中 @Autowired 和 @Resource 都是既使用了名称查找又使用了类型查找,但二者进行查找的顺序却截然相反。
@Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找
@Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找
- 支持的参数不同
二者支持的参数以及参数的个数完全不同,其中 @Autowired 只支持设置一个 required 的参数,而 @Resource 支持 7 个参数
- 依赖注入的支持不同
@Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入