在上一篇文章中我们已经介绍在XML文件注册Bean的具体步骤,这一篇文章将会介绍使用更加简洁的方式(使用注解)来存储和读取Bean.这也是最常用的方法.
1. 创建并配置好Spring项目
和上一篇的步骤相同,下面就相当于复习如何创建Spring项目吧
- 创建一个 Maven 项目
- 为 Spring 项目添加依赖包(spring-beans/spring-context)
- 创建一个启动类
- 为 Spring 项目 创建配置文件(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">
</beans>
2. 存储 Bean
之前存储 Bean 时需要根据我们的需要,添加对应的 Bean 注册内容 才能完成注入, 而现在我们只需要一行内容就可以替代之前的写多行注册内容.那么为了完成这个简单的注入,实现要配置好扫描路径.
2.1 配置扫描路径
扫描路径: 决定 Spring 搜索的文件范围, 默认是从Java文件的根路径开始搜索
根路径: 在IDEA中可以修改文件的属性, 修改为不同类型文件的根目录, 对应的Java文件的根目录的颜色如图是浅蓝色的.
比如你在根目录下把需要注入到Spring中的对象创建到 "com.bean.controller"中,那么对应的扫描路径就应该为
<content:component-scan base-package="com.bean.controller"></content:component-scan>
注:
- 只有在当前扫描包下的对象并且添加了注解的类才会被存储到Spring中
- 同时使用注解注入和bean内容注入是不会发生冲突的
2.2 使用注解注入 Bean
通过两种注解方式使对象注入到Spring中
- 类注解: @Controller、@Service、@Repository、@Component、@Configuration
- 方法注解: @Bean
2.2.1 不同类注解的作用
相信大家想知道为什么有这么多类注解? 虽然最终完成的工作都是把对象注入,但是它们之间的关系就类似于不同地区的车牌号一样,不同地区的车牌号是不同的,这样就能够直观的辨识一辆车的归属地, 这里的类注解就是为了让程序员直观的了解当前类的用途,例如:
- @Controller:业务逻辑层(验证前端传递的数据的合法性)
- @Servie:服务层(服务调用的编排和汇总)
- @Repository:持久层 (直接操控数据库)
- @Configuration:配置层 (关于项目的使所有配置)
- @Component: 组件(通用化的工具类)
程序之间的层次关系如下:
2.2.2 类注解之间的联系
通过查看五大注解的源码,可以发现它们都是@Component的子类.作用都是把 Bean 存储到Spring中
2.2.3 五大类注解使用示例
为了区分不同注解之间的关系,创建不同的包来验证其效果
那么当前项目的扫描路径就是从"com.bean"下的所有包
<content:component-scan base-package="com.bean"></content:component-scan>
- @Component 获取对象
package com.bean.component;
import org.springframework.stereotype.Component;
@Component
public class UserComponent {
public void doComponent() {
System.out.println("Do Component");
}
}
- @Configuration 获取对象
package com.bean.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfiguration {
public void doConfiguration() {
System.out.println("Do Configuration");
}
}
- @Controller 获取对象
package com.bean.controller;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
public void doController() {
System.out.println("Do Controller");
}
}
- @Repository 获取对象
package com.bean.repository;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
public void doRepository() {
System.out.println("Do Repository");
}
}
- @Service 获取对象
package com.bean.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void doService() {
System.out.println("Do Service");
}
}
- 启动类
import com.bean.component.UserComponent;
import com.bean.config.UserConfiguration;
import com.bean.controller.UserController;
import com.bean.repository.UserRepository;
import com.bean.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
// 1. 获取上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
// 2. 使用 getBean 方法获取对象
UserComponent userComponent = context.getBean("userComponent", UserComponent.class);
UserConfiguration userConfiguration = context.getBean("userConfiguration", UserConfiguration.class);
UserController userController = context.getBean("userController", UserController.class);
UserRepository userRepository = context.getBean("userRepository", UserRepository.class);
UserService userService = context.getBean("userService", UserService.class);
// 3. 使用 Bean
userComponent.doComponent();
userConfiguration.doConfiguration();
userController.doController();
userRepository.doRepository();
userService.doService();
}
}
成功运行截图:
2.2.4 Bean 的命名规则
在Java中通常对类名的命名规则是大驼峰命名(例如: UserComponent, UserConfiguration等), 而在读取时使用的是首字母小写获取 Bean (例如:userComponent, userConfiguration等),但是这是普遍规则吗?
假如现在的类名的首字母和第二个字母都是大写时是否满足呢?
package com.bean.component;
import org.springframework.stereotype.Component;
@Component
public class UComponent {
public void doComponent() {
System.out.println("Do Component");
}
}
使用首字母小写的方法结果是错误的,那么就需要知道 Bean 的命名规则
- 在 Idea中搜索 BeanName
- 点击进入查看, 找默认生成 Bean 名称方法
- 最后发现使用的名称生成器是JDK中 Introspector类中的方法
通过查看源代码得出结论:
- 当类名的前两个字母都是大写的情况下, 那么 Bean 的名称默认为原类名
- 其他情况都是首字母小写为 Bean 名称
知道问题的解决方案后,修改后就能够成功读取了
2.2.5 方法注解 @Bean
前面学习类注解是使用在方法上的,那么方法注解是不是使用在方法上呢?
package com.bean.model;
import org.springframework.context.annotation.Bean;
public class User {
public String name;
public int age;
@Bean
public User user1() {
User user = new User();
user.setAge(10);
user.setName("李四");
return user;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
我们发现尝试获取当前 Bean 时发生了异常
原来方法注解是需要搭配类注解共同完成注入的
同样的通过@Bean 注解 Bean 名称生成规则和前面一样,不同的是 @Bean 可以有多个名称,我们通过查看源码就可以发现,
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};
@AliasFor("value")
String[] name() default {};
/** @deprecated */
@Deprecated
Autowire autowire() default Autowire.NO;
boolean autowireCandidate() default true;
String initMethod() default "";
String destroyMethod() default "(inferred)";
}
其 name是 一个String[]数组,也就意味着可以有0个或者多个名称.
@Bean(name = {"u1", "u2"})
public User user() {
User user = new User();
user.setAge(10);
user.setName("李四");
return user;
}
并且name={}可以省略
@Bean({"u1", "u2"})
public User user() {
User user = new User();
user.setAge(10);
user.setName("李四");
return user;
}
注: 对 Bean 重命名后就不能使用方法名获取 对象 了
2.2.6 注解方式的对比
相同点:
- 都是把通过注解的方式把对象存储到Spring容器中
不同点:
- @Component :通用的注解,可标注任意类为 Spring 的组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注
- @Configuration :声明该类为一个配置类,可以在此类中声明一个或多个 @Bean 方法。
- @Controller :对应 Spring MVC 控制层,主要用来接受用户请求并调用 Service 层返回数据给前端页面
- @Service :对应服务层,主要设计一些复杂的逻辑,需要用到 Dao 层
- @Repository :对应持久层即 Dao 层,主要用于数据库相关操作。
- 类注解是通过路径扫描来自动侦测以及自动装配到 Spring 容器中,@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean 告诉了 Spring 这是某个类的实例并且产生这个 Bean 对象的方法Spring 只会调用一次,随后Spring 就会把该对象存储在自己的IoC容器中.
- @Component , @Repository , @ Controller , @Service 这些注解只局限于自己编写的类,而@Bean注解能把第三方库中的类实例加入IOC容器中并交给Spring管理
3. 获取 Bean
获取 Bean 也叫做 对象装配, 是把对象取出来放入某个类中, 有时候也叫做 对象注入, 其实现方式有下面三种
- 属性注入 (Field Injection)
- 构造方法注入 (Constructor Injection)
- Setter注入 (Setter Injection)
下面采用的是将 Service 类注入到Controller类中
3.1 属性注入
package com.bean.controller;
import com.bean.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
// 1. 属性注入
@Autowired
private UserService userService;
public void doController() {
System.out.println("Do Controller");
userService.doService();
}
}
运行成功截图:
3.1.1 优点
实现简单,只需要把需要的属性上加入@Autowired 注解即可
3.1.2 缺点
- 功能性问题
使用属性注入无法注入一个不可变对象(被final修饰对象)
原因: 在 Java中的 被 final 修饰的变量 要么直接初始化,要么使用构造方法初始化,当使用属性注入时都没有满足上面任一条件当然会出错
- 通用性问题
使用属性注入只适用于IoC框架, 如果在其他非IoC容器中使用就无法使用了
- 设计原则问题
因为属性注入的简易,所有可能会导致开发者在一个类中注入多个类,但是对于这些类是否需要呢? 所以有很大可能会违反单一设计原则.
3.2 Setter 注入
@Controller
public class UserController {
private UserService userService;
// 2. Setter 注入
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void doController() {
System.out.println("Do Controller");
userService.doService();
}
}
3.2.1 优点
因为一个Setter方法只会针对一个对象,所以它的优点很明显: 符合单一设计原则
3.2.2 缺点
- 功能性问题
使用属性注入无法注入一个不可变对象(被final修饰对象)
- 注入对象可以被修改
对于当前类setXXX()方法是可见的,所以你可以在某处调用setXXX()方法从而改变注入对象
3.3 构造方法注入
package com.bean.controller;
import com.bean.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
// 3. 构造方法注入
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
public void doController() {
System.out.println("Do Controller");
userService.doService();
}
}
注:
- 如果当前类只有一个构造方法那么 @Autowired 可以省略
- 如果存在多个属性需要注入,那么只有被@Autowired 注解的构造方法可以成功注入,并且有且仅有一个构造方法可以被@Autowired注解
- 可以在一个构造方法中注入多个对象
- 对于注入的类,要确保不为空,否则会报错
对于List集合类含有默认的构造器不为空,但是Integer类的默认构造器需要参数,所以报错
在Spring 4.2 之前官方推荐使用注入方法是Setter注入,因为Setter更符合单一设计原则; 但在Spring 4.2 后官方推荐使用构造方法注入的方式, 因为构造方法有以下优点:
- 可以注入不可变对象
原因是通过构造方法注入就符合Java设计规范
- 注入对象不会被改变
- 因为构造方法只会执行一次
- 完全初始化
- 因为构造方法是在对象创建前优先调用的,所以注入对象在使用前一定被完全初始化
- 通用性更好
- 因为构造方法是Java最底层的框架, 所以在不同的框架下都能使用
3.4 @Resource 另一种注入关键字
@Resource 有两种注入方式:
- 属性注入
package com.bean.controller;
import com.bean.service.UserService;
import org.springframework.stereotype.Controller;
import javax.annotation.Resource;
@Controller
public class UserController2 {
// 1. 属性注入
@Resource
private UserService userService;
public void doController() {
System.out.println("Do Controller 2.0");
userService.doService();
}
}
- Setter()注入
package com.bean.controller;
import com.bean.service.UserService;
import org.springframework.stereotype.Controller;
import javax.annotation.Resource;
@Controller
public class UserController2 {
//2. Setter()注入
private UserService userService;
@Resource
public void setUserService(UserService userService) {
this.userService = userService;
}
public void doController() {
System.out.println("Do Controller 2.0");
userService.doService();
}
}
3.5 @Resource 和 @Autowired的异同点
- 相同点
@Autowired 和 @Resource都可以用来装配bean,都可以用于属性注入或setter()注入
- 不同点
- 来源不同: @Autowired 是Spring 提供的, @Resource是JDK的注解
- @Autowire 默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false
- @Resource 默认按名称装配,当找不到与名称匹配的 bean 时才按照类型进行装配。名称可以通过 name 属性指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名,当注解写在 setter 方法上时,默认取属性名进行装配.
- 装配顺序不同.
Autowired的装配顺序 | 只根据type进行注入,不会去匹配name |
---|---|
Resource 的装配顺序 | 1. 如果同时指定 name 和 type,则从容器中查找唯一匹配的 bean 装配,找不到则抛出异常; 2. 如果指定 name 属性,则从容器中查找名称匹配的 bean 装配,找不到则抛出异常; 3. 如果指定 type 属性,则从容器中查找类型唯一匹配的 bean 装配,找不到或者找到多个抛出异常; 4. 如果不指定,则自动按照 byName 方式装配,如果没有匹配,则回退一个原始类型进行匹配,如果匹配则自动装配 |
注:
可以使用@Qualifier 指定名称,下面的两种写法效果是相同的
@Autowired(required = false) @Qualifier("userService")
private UserService userService;
@Resource(name = "userService")
private UserService userService;