第二个 Spring 项目
在上面的第一个 Spring 项目中,我们是通过将 bean 对象通过配置文件注册到 spring 中,之后再从配置文件中取出对象来,这一部分需要我们手动去输入 " id " 和 " class " 属性。
而在我们即将实现的第二个 Spring 项目中,我们是完全通过注解的方式进行了,这样一来,就可以更简单的存储对象和读取对象了。
如果说第一个 Spring 项目是一个手动挡的汽车,那么第二个 Spring 项目就是一个自动挡的汽车,因为实际上,一个注解就只有一行代码而已,通过注解这样的方式,很多工作并不需要我们自己动手去做,而是交给 spring 框架去做。
1. 搭建项目环境
(1) 创建一个 maven 项目
(2) 添加 spring 框架支持 ( spring-context + spring-beans )
(3) 配置国内源
(4) 创一个启动类并添加 main 方法
(5) 配置 " spring-config.xml " 文件
前 4 步,在第一个 spring 项目中,已经体现出来了。现在,我们来实现第 5 步。
我们在 spring-config.xml 添加如下配置:
<?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=" "> </content:component-scan> </beans>
其中有一个 " base-package " 这样的属性十分重要!它表明了我们所有存放到 spring 中的 bean 的根路径。
如下图所示,只要我们涉及 spring 的类,就必须放在当前的 " beans " 包下,或者重新创建一个包 " abc " ,把类放在 " abc " 包下,但是 " abc " 包也应该在 " beans " 包下。也就是说," beans " 目录可以是 spring 类的父级目录,也可以是 " 爷爷目录 ",依然可以是 " 祖先目录 " …但不能是子孙目录。
2. 将 bean 对象存储到 spring 容器中
要想简单地将对象存储在 Spring 中,有如下两种注解类型可以实现。
必须明确,两种注解都是 spring 框架提供的,也就是从我们之前从 maven 仓库引入的依赖所提供的。两种注解实际上在底层就是对应着各自的 " .class " 文件。
(1) 类注解:
@Controller、@Service、@Repository、@Component、@Configuration.
(2) 方法注解:
@Bean.
(1) 类注解
创建一个 UserController 类,作为 bean 对象。
在 UserController 类的上方,添加一个 " @Controller " 注解。
@Controller public class UserController { public void hello() { System.out.println("你好,Controller"); } }
在启动类中,我们通过 " getBean " 方法进行测试,结果发现并没有问题。
public class Run { public static void main(String[] args) { // 1. 得到 spring 上下文对象 ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); // 2. 根据上下文对象提供的方法获取到 bean UserController userController = context.getBean("userController", UserController.class); // 3. 使用 userController.hello(); } }
注意
注意1 五个类注解之间的联系
1. 在上面的程序中,我只测试了一个 " @Controller " 注解,如果我们使用其他四个类注解,在当前的打印功能上,它们其实都是一样的,我就不额外展示了。
但是我们应该着重理解这五个类注解,到底都是什么意思?能用来干什么?它们之间有什么区别和联系?
如下图所示,在一个企业项目中,最少会有下面的四层,或许会比四层多,但一定不会比下面的四层少。这四层,我们利用这四个注解来进行解释。
@Configuration:配置层
配置层用于存放当前项目的所有配置,其实在我们日常学习的时候,感受不到配置文件的分层问题,因为,平时我们可能不做项目,就将一个项目的所有文件放在一个目录下。而在实际的工作开发中,一个项目,可能有成千上万个文件,多数情况下,这些目录并不仅仅只有你的代码,也有你同事的代码。而配置层就可以通过一些配置类,来管理这些文件。当我们需要对当前项目的一些配置进行修改、维护的时候,只需要利用这一层来实现即可。
@Controller:控制层
控制层主要用作前端参数校验。比方说,现在我们通过前端登录一个网站,那么我们至少需要输入账号和密码这两个参数供后端验证,那么此时,就可以利用控制层来实现登录逻辑,验证成功后,才能到达下一层 " @Service ",验证失败,直接返回错误给前端用户。
举个例子:这就和当下疫情一样,当你的健康码是绿码的时候,你才能通行,否则,就可能需要马上去做核酸…那么控制层的作用就体现出来了,它就像安检关卡一样。
@Service:服务层
服务层主要实现了数据的组装和接口调用。
@Repository:数据持久层 ( DAO 层 )
数据持久层直接与数据库打交道,在这一层,可以实现数据库的增删查改。
@Component:上面四个注解的 " 父亲 "
查看上面四个注解的源码,就可以发现,它们四个都有 " @Component ",说明它们本身就是基于 " @Component " 实现的,换句话说,它们四个就是 " @Component " 的 " 孩子 "。
总结:一个 spring 项目为什么要分层呢?
举个例子,当我们去某家公司面试,公司会有 Java 面试的地方、前端面试的地方、C++ 面试的地方,这些地方我们可以理解为【配置层】分配好的区域。假设我们去面试 Java 后端工程师,首先,我们需要通过安检,出示我们的健康码,如果是绿码才能正常进入公司,此时这个安检就可以理解为【控制层】。当我们通过了安检,可能就会有服务人员告诉你,Java 面试在某某楼、某某房间,你需要通过他给你的提示路线,才能找到具体的面试地点,此时服务人员就可以理解为【服务层】。只有你到达了最终目的地,面试官才能面试你,那么最后一步,我们才能视为【持久层】。
再回到上面的问题,在实际项目中,我们不可能通过一次执行,就直接对数据库进行操作,这不符合权限、也不安全,更不科学。
注意2 BeanName 的命名规则
打开下面的一个类文件,查看底层类的源码,滑动到页面最后,打开 " decapitalize " 方法,再看其源码。
这一段源码核心地方在于下面的红框部分,如果第一个字母和第二个字母都是大写,那么直接返回;否则,就会将第一个字母变成小写,再返回。 现在,我们使用 " getBean " 方法,传入的参数就有依据了。
此外,值得注意的是,这是 JDK 提供的标准。
在下面,对源码进行测试,最终发现与我们理解的是一致的。
public class test { public static void main(String[] args) { String str1 = "UserController"; String str2 = "APIController"; System.out.println(Introspector.decapitalize(str1)); System.out.println(Introspector.decapitalize(str2)); } }
综上所述,我们在今后使用 " getBean " 方法的时候,就只应该注意我们自己定义类的前两个字母即可,如果前两个字母都是大写,传入的就是原类名;反之,就将第一个字母变成小写即可。
UserController userController = context.getBean("userController", UserController.class); APIController apiController = context.getBean("APIController", APIController.class);
(2) 方法注解
@Controller public class UserBean { @Bean public User user1() { User user = new User(); user.id = 1; user.name = "露丝"; return user; } @Bean(name = {"user", "userinfo"}) public User user2() { User user = new User(); user.id = 2; user.name = "杰克"; return user; } }
注意
(1) " @Bean " 需要和五大类注解配合使用,才能生效。
(2) " @Bean " 只能放在方法上面,将当前方法返回的对象,存储到 spring 容器中。
(3) 使用 " @Bean " 存储到 spring 容器后,再从容器中取出来 bean 对象时,对传入 " getBean " 方法的参数,也有讲究。如果当前没有重命名,就传入 " @Bean " 注释的方法名;如果对 " @Bean " 重命名了,那么就只能使用新的名字,原来的方法名失效。重命名的规则很简单,在 " @Bean " 后面加上 name 数组即可,可以重新使用多个名字。
对上面的程序进行验证:
User user = context.getBean("user1", User.class); // true User user = context.getBean("user2", User.class); // false User user = context.getBean("user", User.class); // true User user = context.getBean("userinfo", User.class); // true
3. 对象装配来获取 bean 对象
以往我们是通过 new 一个对象的传统写法,来从一个类中拿到外部类的对象。现在,我们使用对象装配的方式将一个类的对象放到另一个类中。对象装配也叫做对象注入,实际上就是将 bean 对象取出来放到某个类中,接着就可以在这个类中,直接使用 bean 对象了。
对象注入的实现方法有下面三种:
(1) 属性注入
(2) 构造方法注入
(3) Setter 注入
(1) 属性注入 bean 对象 ( 字段注入 )
我们往 " UserController " 类中,注入 " UserService " 类的对象,之后,我们就可以通过 " UserController " 类拿到 " UserService " 类的字段、方法等数据了。下面的 Run 启动类,只是用来测试一下,对象装配是否成功。
注意
以往我们是通过 new 一个对象的传统写法,来从一个类中拿到外部类的对象。
UserService userService = new UserService();
现在,我们是通过属性注入的方式,拿到外部类的 bean 对象。
但使用属性注入的前提是,我们需要保证被注入的对象是一个 bean 对象,也就是说,它得包含五大类注解才行。
@Autowired private UserService userService;
这就像:一个容器里面装了一个大盒子,这个大盒子又装了一个小盒子。
当我们从 Spring 容器中,将 UserController 这个大盒子取出来的时候,UserService 这个小盒子,也会被取出来。
(2) 构造方法注入 bean 对象
我们往 " UserController2 " 类中,注入 " UserService " 类的对象,之后,我们就可以通过 " UserController2 " 类拿到 " UserService " 类的字段、方法等数据了。下面的 Run 启动类,只是用来测试一下,对象装配是否成功。
注意
注意1
下面这行代码,并不是属性注入,因为它上面并没有 " @Autowired " 这样的注解,所以很明显,它就是是一个值为 null 的一个字段而已。
private UserService userService;
接着,由于构造方法在外部 new 的时候,第一时间就能够使用,也就是说,构造方法的优先级很高。所以,通过构造方法注入,就是将原先值为 null 的 userService 字段,赋值了新的对象,这就好像激活了 UserService 类一样。
@Autowired public UserController2(UserService userService) { this.userService = userService; }
注意2
利用构造方法注入 bean 对象的时候,如果被注入的类出现了多个构造方法,我们只能使用一个构造方法来进行 bean 对象的注入,也就是只将其中一个构造方法设置为注解 " @Autowired "。
(3) Setter 注入 bean 对象
我们往 " UserController3 " 类中,注入 " UserService " 类的对象,之后,我们就可以通过 " UserController3 " 类拿到 " UserService " 类的字段、方法等数据了。下面的 Run 启动类,只是用来测试一下,对象装配是否成功。
注意
Setter 方法注入和构造方法注入的思想基本相同,这里我就不展开介绍了。但是它们之间也略有不同,因为构造方法可能根据参数不同,所以会有多个构造方法,然而,Setter 方法则不同,它只有唯一一个,它只针对于某个字段进行设置。
经典面试题1
对象注入的方式有哪些?
属性注入、构造方法注入、Setter 注入三者的区别?
答:
① 属性注入:写法最简单,但通用性较差,它只能用于 IOC 容器,如果是用于非 IOC 容器下,就会出现空指针异常。
② 构造方法注入:通用性好,此外,由于构造方法是一个类的优先级最高的成员,所以使用构造方法注入,就能够确保注入对象不会出差错,这也是现阶段官方推荐的写法。但是,由于构造方法可能根据参数不同,所以会有多个构造方法,如果多个构造方法用到了同样的对象注入,那么程序就会显得比较冗余。
③ Setter 注入:官方早期的推荐写法,但通用性依旧较构造方法差,因为在 Java 中的 Setter 方法,放在其他语言之中,可能就行不通了。
④ 官方推荐的写法是一种理论,我们在实际开发中,只要符合对应场景,用到最多的还是属性注入,因为理论和实践还是不一样的,实践以高效、实用为主。
经典面试题2
在进行对象注入时,除了可以使用 " @Autowired " 注解之外,我们还可以使用 " @Resource " 进行注入。
对象注入可以使用的哪两种注解 / 哪两种关键字?
① 出身不同:" @Autowired " 来自于 Spring 框架,而 " @Resource " 来自于 JDK.
② 用法不同:注解 " @Autowired " 支持属性注入、构造方法注入 和 Setter 注入,然而,注解 " @Resource " 不支持构造方法注入。
③ 支持的参数不同:" @Autowired " 只支持 required 参数设置,而 " @Resource " 支持更多的参数设置,比如 name、type 设置。
同一个类型的对象注入多次的问题
创建一个 User 类,作为待存储的类。
public class User{ public int id; public String name; @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
在 UserBean 类下,通过注解 " @Bean " ,将 User 类的对象往 spring 容器中注入了两次,一个 beanName 为 " user1 ",一个 beanName 为 " user2 ".
@Controller public class UserBean { @Bean public User user1() { User user = new User(); user.id = 1; user.name = "露丝"; return user; } @Bean public User user2() { User user = new User(); user.id = 2; user.name = "杰克"; return user; } }
创建一个 UserController4 类,进行 User 类的对象注入。
@Controller public class UserController4 { @Autowired private User user; public void hello() { System.out.println("你好," + user.name); } }
利用 Run 启动类,进行测试。
public class Run { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); UserController4 userController4 = context.getBean("userController4", UserController4.class); userController4.hello(); } }
通过 Run 类测试,我们发现,如果我们直接注入一个 beanName 为 " user " 的一个对象,就会出现如下错误。这其实很好理解,因为当初存入的对象,一个名为 " user1 " ,一个名为 " user2 ",现在,我们却让 spring 容器为我们找一个名称不匹配的对象,自然是找不到的,因为容器中 User 类的对象不唯一。
备注: 如果在 Spring 容器中,同一个类型的对象只有一个,那么,就算我们对象注入的名字与那唯一对象的名字不匹配,也不影响 spring 能够找出来。然而,同一个类型,有多个对象,就算 spring 再智能,它也不知道你究竟想要哪个了。
解决方案
综上所述,我们就知道了,往 spring 中存入 " bean 对象 " 的时候,没有发生问题,但是,从 spring 中取出 " bean 对象 " 的时候,却发生了问题。那么,我们只需要修改【对象注入】的弊端即可。
方案1 精确描述 beanName
如果你想要 user1,属性注入的时候就直接注入 user1;
同样地,如果你想要 user2,就直接注入 user2.
@Autowired private User user1; public void hello() { System.out.println("你好," + user1.name); }
测试结果:
@Autowired private User user2; public void hello() { System.out.println("你好," + user2.name); }
方案2 通过 " @Resource " 设置 【name 参数】 来重命名
之前,我们提到 " @Autowired " 和 " @Resource ",发现 " @Resource " 在提供的参数方面,更胜一筹。所以我们直接利用其 【name 参数】进行重命名设置即可,这样,就相当于告诉了 spring 容器,你到底取的是哪个对象了。
@Resource(name = "user1") private User user; public void hello() { System.out.println("你好," + user.name); } // 你好,露丝
@Resource(name = "user2") private User user; public void hello() { System.out.println("你好," + user.name); } // 你好,杰克
方案3 通过 " @Autowired " + " @Qualifier " 的方式来限定名称
实际上, " @Qualifier " 注解只有一个参数,就是 value,可写可不写。但我们最好还是加上 value,因为这样更准确,此外,如果有一天,spring 框架升级了 " @Qualifier " 注解,让它不止一个参数,那么我们也能很好地预防意外。
@Autowired @Qualifier(value = "user1") private User user; public void hello() { System.out.println("你好," + user.name); } // 你好,露丝
@Autowired @Qualifier(value = "user2") private User user; public void hello() { System.out.println("你好," + user.name); } // 你好,杰克
总结
之前,我们说第一个 spring 项目需要依赖 " xml " 配置文件,来进行存储和取出 bean 对象的,这就像开手动挡的车一样。之后,在第二个 spring 项目中,我们又通过注解的方式,来进行存储和读取 bean 对象,这时候,就像开自动挡的车子一样了。
而实际上,这就是一个随着时代发展,技术升级的过程,后面还有更现代的框架可供使用。但是,我们需要记住一点,在当前的 spring 框架中,核心功能就是存储和读取 bean 对象,所以,项目不管以什么方式实现,牢记核心才是至上的。