经过前面的学习,我们已经可以实现基本的 Spring 读取和存储对象的操作了,但在操作的过程中我们发现读取和存储对象并没有想象中的那么 “简单”,所以接下来我们要学习更加简单的操作 Bean 对象的方法
在 Spring 中想要更简单的存储和读取对象的核心是使用注解,也就是我们接下来要学习 Spring 中的相关注解,来存储和读取 Bean 对象
之前我们存储 Bean 时,需要在 spring-config 中添加⼀⾏ bean 注册内容才⾏:
<bean id="user" class="com.beans.User"></bean>
而现在我们只需要⼀个注解就可以替代之前要写一行配置的尴尬了,不过在开始存储对象之前,我们先要来点准备工作。
注意:想要将对象成功的存储到 Spring 中,我们需要配置⼀下存储对象的扫描包路径,只有被配置的包下的所有类,添加了注解才能被正确的识别并保存到 Spring 中。
在 resources 中创建 spring-config.xml 文件
——在 spring-config.xml 中设置 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=""></content:component-scan>
</beans>
在 java 中创建一个 package:com.beans
,这就是所有要存放到 spring 中的 bean 的根路径
也就是说,即使添加了注解,如果不是在配置的扫描包下的类对象,也是不能被存储到 Spring 中的,
注意: 只会扫描对应的目录,包括在根目录下的子目录的所有类都可以被扫描
想要将对象存储在 Spring 中,有两种注解类型可以实现:
类注解:
@Controller 控制器
@Service 服务
@Repository 仓库
@Component 配置
@Configuration 组件
方法注解:
@Bean
在 com.beans 下创建一个类
UserController
使⽤ @Controller 存储 bean 的代码如下所示: @Controller
不能省略
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
public void sayHi() {
System.out.println("hello, UserController");
}
}
此时我们先使⽤之前读取对象的⽅式来读取上⾯的 UserController 对象,如下代码所示:
使⽤ @Service 存储 bean 的代码如下所示
@Service
public class UserService {
public void sayHi() {
System.out.println("hello, UserService! ");
}
}
读取 bean 的代码:
public class App {
public static void main(String[] args) {
// 1、先得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2、得到 bean 对象
UserService service = context.getBean("userService", UserService.class);
// 3、使用 bean
service.sayHi();
}
}
使⽤ @Repository 存储 bean 的代码如下所示:
@Repository
public class UserRepository {
public void sayHi() {
System.out.println("hello, UserRepository");
}
}
读取 bean 的代码:
public class App {
public static void main(String[] args) {
// 1、先得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2、得到 bean 对象
UserRepository repository = context.getBean("userRepository", UserRepository.class);
// 3、使用 bean
repository.sayHi();
}
}
使⽤ @Component 存储 bean 的代码如下所示:
@Component
public class UserComponent {
public void sayHi() {
System.out.println("hello, UserComponent");
}
}
读取 bean 的代码:
public class App {
public static void main(String[] args) {
// 1、先得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2、得到 bean 对象
UserComponent component = context.getBean("userComponent", UserComponent.class);
// 3、使用 bean
component.sayHi();
}
}
使⽤ @Configuration 存储 bean 的代码如下所示:(注意不是 @Configurable)
@Configuration
public class UserConfig {
public void sayHi() {
System.out.println("hello, UserConfig");
}
}
读取 bean 的代码:
public class App {
public static void main(String[] args) {
// 1、先得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2、得到 bean 对象
UserConfig config = context.getBean("userConfig", UserConfig.class);
// 3、使用 bean
config.sayHi();
}
}
既然功能是⼀样的,为什么需要这么多的类注解呢?
让代码的可读性提高,让程序员能够直观的判断当前类的用途。
这和为什么每个省/市都有⾃⼰的⻋牌号是⼀样的?⽐如陕⻄的⻋牌号就是:陕X:XXXXXX,北京的⻋牌号:京X:XXXXXX,⼀样。甚⾄⼀个省不同的县区也是不同的,⽐如⻄安就是,陕A:XXXXX,咸阳:陕B:XXXXXX,宝鸡,陕C:XXXXXX,⼀样。这样做的好处除了可以节约号码之外,更重要的作⽤是可以直观的标识⼀辆⻋的归属地。
那么为什么需要怎么多的类注解也是相同的原因,就是让程序员看到类注解之后,就能直接了解当前类
的⽤途,⽐如:
程序的⼯程分层,调⽤流程如下:
查看 @Controller / @Service / @Repository / @Configuration 等注解的源码发现:
其实这些注解⾥⾯都有⼀个注解 @Component,@Controller / @Service / @Repository / @Configuration 是基于 @Component 实现的,@Component 可以认为是其他 4 个注解的父类
创建类 APIController,使用 getBean() 获取 bean 对象,如果 beanName 使用首字母小写,出现错误:
如果使用原类名,运行成功:
APIController apiController = context.getBean("APIController", APIController.class);
——在 Idea 中使⽤搜索关键字“beanName”可以看到以下内容:
Spring 原码与注释
——查看原码:
——来到了 .java 文件,说明不是 spring 的方法,打开当前类所在的目录:
说明 spring 生成 beanName 的方法是 JDK 自身的方法
——bean 对象的命名规则的⽅法,使⽤的是 JDK Introspector 中的 decapitalize ⽅法,源码如下:
验证:
import java.beans.Introspector;
public class App2 {
public static void main(String[] args) {
String className1 = "UserController";
String className2 = "APIController";
System.out.println(Introspector.decapitalize(className1)); // userController
System.out.println(Introspector.decapitalize(className2)); // APIController
}
}
类注解是添加到某个类上的,⽽⽅法注解是放到某个⽅法上的,
创建类 User,添加 Getter && Setter 和 toString 方法::
package com.beans;
public class User {
private int id;
private String name;
}
创建类 UserBeans,添加一个 user1 方法:
public class UserBeans {
@Bean
public User user1() {
User user = new User();
user.setId(1);
user.setName("zhangsan");
return user;
}
}
获取 bean 对象: 方法注解的 beanName 是方法名
使用类型获取 bean: 可以通过类型获取 bean 对象,但是添加一个 user2 方法,就会报错
说明没有成功将对象注入到 spring 中,在 Spring 框架的设计中,方法注解 @Bean 要配合类注解才能将对象正常的存储到 Spring 容器中
修改代码:
@Component
public class UserBeans {
@Bean // 注意:只使用一个 @Bean 是无法将对象存储到容器中的
public User user1() {
User user = new User();
user.setId(1);
user.setName("zhangsan");
return user;
}
}
运行结果: User{id=1, name=‘zhangsan’}
可以通过设置 name
属性给 Bean 对象进行重命名操作,如下代码所示:
@Component
public class UserBeans {
@Bean(name = "userInfo")
public User user1() {
User user = new User();
user.setId(1);
user.setName("zhangsan");
return user;
}
}
使用:
public class App {
public static void main(String[] args) {
// 1、先得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
User user = context.getBean("userInfo", User.class);
System.out.println(user);
}
}
这个重命名的 name 其实是⼀个数组,⼀个 bean 可以有多个名字,@Bean(name = {"userInfo", "userTest"})
并且 name= 可以省略 @Bean({"userInfo", "userTest"})
注意: 重命名之后,使用原来的方法名是否能正确获取到对象?
User user = context.getBean("user1", User.class);
报错:No bean named ‘user1’ available
从容器中获取 bean 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注入
实现方法有以下三种:
属性注入
构造方法注入
Setter 注入
属性注入是使用 @Autowired 实现的,将 Service 类注⼊到 Controller 类中
——UserController:
/**
* 根据属性实现 bean 对象的注入
*/
@Controller
public class UserController {
@Autowired
private UserService userService;
public void sayHi() {
userService.sayHi();
}
}
告诉 spring 在加载 UserController 时,先将 UserService 对象,注入当前类的 userService 属性中。前提是 UserService 已经被注入到 spring 中,
——UserService:
@Service
public class UserService {
public void sayHi() {
System.out.println("hello, UserService");
}
}
——验证:
运行结果:hello, UserService
public class App {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController userController = context.getBean(UserController.class);
userController.sayHi();
}
}
(官方推荐的写法)
如果当前类中只存在一个构造方法,那么@Autowired 注解可以省略
——UserController2:
/**
* 使用构造方法实现 bean 注入
*/
@Controller
public class UserController2 {
private UserService userService;
@Autowired
public UserController2(UserService userService) {
this.userService = userService;
}
public void sayHi() {
userService.sayHi();
}
// 传统写法
// public UserController2() {
// userService = new UserService();
//
//}
}
——验证:
运行结果:hello, UserService
public class App {
public static void main(String[] args) {
// 1、得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController2 userController2 = context.getBean(UserController2.class);
userController2.sayHi();
}
}
Setter 注入和属性的 Setter 方法实现类似,只不过在设置 set ⽅法的时候需要加上 @Autowired 注解
——UserController3:
@Controller
public class UserController3 {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sayHi() {
userService.sayHi();
}
}
——验证:
运行结果:hello, UserService
public class App {
public static void main(String[] args) {
// 1、得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
UserController3 userController3 = context.getBean(UserController3.class);
userController3.sayHi();
}
}
属性注⼊: 优点是简洁,使⽤⽅便;缺点是只能⽤于 IoC 容器,如果是⾮ IoC 容器不可⽤,并且只有在使⽤的时候才会出现 NPE(空指针异常)
构造⽅法注⼊: 是 Spring 推荐的注⼊⽅式,它的缺点是如果有多个注⼊会显得⽐较臃肿,但出现这种情况你应该考虑⼀下当前类是否符合程序的单⼀职责的设计模式了,它的优点是通⽤性,在使⽤之前⼀定能把保证注⼊的类不为空
Setter ⽅式: 是 Spring 前期版本推荐的注⼊⽅式,但通⽤性不如构造⽅法,所有 Spring 现版本已经推荐使⽤构造⽅法注⼊的⽅式来进⾏类注⼊了
将以上三种对象注入的实现改成 @Resource:属性注入,Setter 注入可以正常运行,而构造方法注入出现问题:
两种注入方法@Autowired 和 @Resource 的区别:
出身不同: @Autowired 来⾃于 Spring,⽽ @Resource 来⾃于 JDK 的注解;
使⽤时支持设置的参数不同: @Autowired 只支持 required 参数设置,相⽐于 @Autowired 来说,@Resource ⽀持更多的参数设置,例如 name, type 设置,根据名称获取 Bean
用法不同: @Resource支持属性注入和 Setter 注入,但不支持构造方法注入
注入一个 User 对象,运行观察是否可以正常打印
UserController
@Controller
public class UserController {
@Resource
public User user;
public void sayHi() {
System.out.println("User —— " + user);
}
}
UserBeans
@Component
public class UserBeans {
@Bean(name = "userInfo")
public User user1() {
User user = new User();
user.setId(1);
user.setName("zhangsan");
return user;
}
@Bean
public User user2() {
User user = new User();
user.setId(1);
user.setName("lisi");
return user;
}
}
App
public class App {
public static void main(String[] args) {
// 1、得到上下文对象
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2、通过上下文对象的 getBean 方法获取 bean 对象
UserController controller = context.getBean(UserController.class);
// 3、使用 bean
controller.sayHi();
}
}
报错信息: No qualifying bean of type ‘com.beans.User’ available: expected single matching bean but found 2: userInfo,user2
1、精确地描述 bean 的名称(将注入的名称写对)。
修改代码:
@Controller
public class UserController {
@Resource
public User user2;
public void sayHi() {
System.out.println("User —— " + user2);
}
}
运行结果: User —— User{id=1, name=‘lisi’}
2、使用 @Resource 设置 name 的方式来重命名注入对象
修改代码:
@Controller
public class UserController {
@Resource(name = "user2")
public User user;
public void sayHi() {
System.out.println("User —— " + user);
}
}
3、使用 Autowired +Qualifier 来筛选 bean 对象
value 可以省略:@Qualifier(“user2”),不建议省略
@Controller
public class UserController {
@Autowired
@Qualifier(value = "user2")
public User user;
public void sayHi() {
System.out.println("User —— " + user);
}
}
综合练习
在 Spring 项⽬中,通过 main ⽅法获取到 Controller 类,调⽤ Controller ⾥⾯通过注⼊的⽅式调⽤Service 类,Service 再通过注⼊的⽅式获取到 Repository 类,Repository 类⾥⾯有⼀个⽅法构建⼀个User 对象,返回给 main ⽅法。Repository ⽆需连接数据库,使⽤伪代码即可