一.存储Bean对象
1.1 <bean></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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="User"> </bean> <bean id="people" class="demo.People"></bean> </beans>
class路径是以java为当前路径来写的
此时只是将类配置到了配置文件中,并没有真正的放到Spring中,只有在获取上下文对象时,才会将对象放进去。
获取bean对象:
public static void main(String[] args) { //先得到 spring 上下文对象(路径名称要和spring创建的配置文件名称相等) ApplicationContext context=new ClassPathXmlApplicationContext("spring-config.xml"); //依赖查找->IoC的一种实现方式 //通过Bean名称来获取(这里的名称要和配置文件中的id)相等,此时得到的object对象,需要强转 User user=(User)context.getBean("user"); //通过类型来获取bean对象(如果Spring容器中有多个该类型对象,会报错) User user1=context.getBean(User.class); //通过Bean名称和类型来获取 User user2=context.getBean("user",User.class); System.out.println(user.sayHello()); // People people=context.getBean("people",People.class); // System.out.println(people.sayHi()); } //User Constructor //hello world public static void main(String[] args) { //XmlBeanFactory 有横线说明被官方弃用了,不建议使用 BeanFactory context=new XmlBeanFactory(new ClassPathResource("spring-config.xml")); //通过Bean名称来获取(这里的名称要和配置文件中的id)相等,此时得到的object对象,需要强转 User user=(User)context.getBean("user"); //通过类型来获取bean对象 User user1=context.getBean(User.class); //通过Bean名称和类型来获取 User user2=context.getBean("user",User.class); System.out.println(user.sayHello()); //People people=context.getBean("people",People.class); //System.out.println(people.sayHi()); } //User Constructor //People constructor //hello world
ApplicationContext VS BeanFactory 有什么区别?
相同点:都是容器管理对象,都可以获取Bean对象。
区别:
• ApplicationContext 是 BeanFactory 的子类,ApplicationContext 拥有更多的功能(对国际化⽀持、资源访问⽀持、以及事件传播等⽅⾯的⽀持)
• Bean 的加载机制不同:ApplicationContext 是一次加载并初始化所有的Bean对象,而BeanFactory 是需要哪个才去加载哪个(懒加载),因此更加轻量。
1.2 配置扫描路径
在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"> <!-- <bean id="user" class="User"> </bean>--> <!-- <bean id="people" class="demo.People"></bean>--> <!-- 配置bean的扫描根路径:只有当前目录下的类才会扫描是否添加了注解,如果添加了注解--> <content:component-scan base-package="demo"></content:component-scan> </beans>
注意:想要将对象成功的存储到Spring 中,我们需要配置一下存储对象的扫描包路径,只有被配置的包下的所有类,添加了注解才能被正确的识别并保存到Spring中
二. 通过注解添加Bean对象
想要将对象存储在Spring中,有两种注解类型可以实现:
2.1 类注解:@Controller、@Service、@Repository、@Component、@Configuration
• @Controller【控制器】校验参数的合法性(安检系统),负责接口对接。
• @Service【业务】业务组装(客服中心):会告诉我使用哪些方法,调用哪些接口,但是不具体实行。
• @Repository【数据持久层】实际业务处理(实际办理的业务):即业务层调用的方法的具体实现,操作一些和数据库相关的信息。
• @Component【组件】 工具类层(基础的工具):例如对账户加密,进行的业务不涉及数据库
• @Configuration 【配置层】配置:例如一些端口号
这五个类注解都可以使用 //@Controller //@Service //@Repository //@Configuration @Component public class People { public People(){ System.out.println("People constructor"); } public void sayHi(){ System.out.println("hello world"); } } public static void main(String[] args) { ApplicationContext context=new ClassPathXmlApplicationContext("spring-config.xml"); People people=context.getBean("people",People.class); people.sayHi(); }
Bean 生成名称源代码(此处是调用的jdk中的命名方法,并不是spring定义的,是Java定义的)
public static String decapitalize(String name) { if (name == null || name.length() == 0) { return name; } //如果name长度大于1,并且前两个字符都是大写,返回name本身 if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))){ return name; } char[] chars = name.toCharArray(); //其他的都是将第一个大写字母改为小写 chars[0] = Character.toLowerCase(chars[0]); return new String(chars); }
类注解Bean命名规则:
• 默认情况下,当类名的前两个字母都是大写时,名字和类名相同;当类名的前两个字母只有第一个是大写时,则名字是类名首字母小写。
• 若通过value设置,那么只能通过设置的类名来获取
五大类注解之间的关系
查看@Controller/@Service/@Repository/@Configuration 源码发现:
其他的四个类都是基于@Component实现的
2.2 方法注解:@Bean
注意:@Bean 必须配合五大类注解一同使用,否则就会报错. (如果不使用五大类注解,spring就会扫描所有的类中的方法,会大大的降低效率).
public class ArticleInfo { private int id; private String title; private String Content; private LocalDateTime CreateTime; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return Content; } public void setContent(String content) { Content = content; } public LocalDateTime getCreateTime() { return CreateTime; } @Override public String toString() { return "ArticleInfo{" + "id=" + id + ", title='" + title + '\'' + ", Content='" + Content + '\'' + ", CreateTime=" + CreateTime + '}'; } public void setCreateTime(LocalDateTime createTime) { CreateTime = createTime; } } @Controller public class Articles { // 可以通过以下三种方式来命名,可以命名任意个名字,若命名后,默认命名(方法名)不能再使用 // @Bean({"arti","article"}) // @Bean(name = {"arti","article"}) @Bean(value = {"arti","article"}) public ArticleInfo art(){ //伪代码(真实的代码中是不允许我们去new的,因为我们IoC的思想就是控制反转) ArticleInfo articleInfo=new ArticleInfo(); articleInfo.setId(1); articleInfo.setTitle("西游记"); articleInfo.setContent("大闹天空"); articleInfo.setCreateTime(LocalDateTime.now()); return articleInfo; } } public static void main(String[] args) { ApplicationContext context=new ClassPathXmlApplicationContext("spring-config.xml"); // ArticleInfo articleInfo=context.getBean("article",ArticleInfo.class); // ArticleInfo articleInfo=context.getBean("arti",ArticleInfo.class); ArticleInfo articleInfo=context.getBean("art",ArticleInfo.class); System.out.println(articleInfo); }
• @Bean的默认获取方式:@Bean的默认命名=方法名
2.3 @Bean的加载顺序
2.3.1 名称相同
//此处的Order中的数字越大,权重越高,优先执行 @Order(100) @Controller public class Articles { // 可以通过以下三种方式来命名,可以命名任意个名字,若命名后,默认命名不能再使用 // @Bean({"arti","article"}) // @Bean(name = {"arti","article"}) @Bean(value = {"arti","article"}) public ArticleInfo arti(){ //伪代码(真实的代码中是不允许我们去new的,因为我们IoC的思想就是控制反转) ArticleInfo art=new ArticleInfo(); art.setId(2); art.setTitle("三国演义"); art.setContent("温酒斩华雄"); art.setCreateTime(LocalDateTime.now()); return art; } @Bean(value = {"arti","article"}) public ArticleInfo art(){ //伪代码(真实的代码中是不允许我们去new的,因为我们IoC的思想就是控制反转) ArticleInfo art=new ArticleInfo(); art.setId(1); art.setTitle("西游记"); art.setContent("大闹天空"); art.setCreateTime(LocalDateTime.now()); return art; } } @Order(18) @Component public class Articles2 { @Bean(value = {"arti","article"}) public ArticleInfo arti(){ //伪代码(真实的代码中是不允许我们去new的,因为我们IoC的思想就是控制反转) ArticleInfo art=new ArticleInfo(); art.setId(3); art.setTitle("红楼梦"); art.setContent("我也不知道"); art.setCreateTime(LocalDateTime.now()); return art; } }
在同一个类中,返回同名的相同类型的方法 Bean 时, 按照前后顺序执行
如果一个类的多个Bean使用相同的名称,那么程序执行不会报错,但是第一个Bean之后的对象不会放到容器中,也就是只有第一次创建Bean的时候会将对称的Bean 名称关联起来,后续再有相同名称Bean,容器会自动忽略
2.3.2 名称不同
对于同种类型的Bean,而名称不同时,仍可注入到容器中
三. 获取Bean 对象
对于之前我们获取 Bean 对象的方法,是通过依赖查找的方法,先获取上下文对象(容器对象)context,然后在通过getBean来获取对象的实例.对此,似乎还没有直接new来的快.因此我们还可以通过对象(依赖)注入的方式来获取对象的实例.
3.1 属性注入
@Repository public class UserResposity { private int num; public int add(){ System.out.println("Do UserResposity method"); return num; } } @Service public class UserService { @Autowired UserResposity userResposity; public int add(){ System.out.println("Do UserService method."); return userResposity.add(); } } //使用测试验证对象是否被注入 class UserServiceTest { @Test public void test1(){ //此处只是为了验证UserService中的UserResposity是否被注入了对象 ApplicationContext context= new ClassPathXmlApplicationContext("spring-config.xml"); UserService userService=context.getBean("userService",UserService.class); userService.add(); } }
上述代码该类型的对象在spring容器中只有一个,那若有多个呢?
public class User { private String name; public void setName(String name) { this.name = name; } @Override public String toString() { return "Users{" + "name='" + name + '\'' + '}'; } } @Component public class Users { @Bean("user1") public User user1(){ User user=new User(); user.setName("张三"); return user; } @Bean("user2") public User user2(){ User user=new User(); user.setName("李四"); return user; } } @Service public class UserService2 { @Autowired User user; public void sayHi(){ System.out.println(user.toString()); } } class UserService2Test { @Test public void test(){ ApplicationContext context= new ClassPathXmlApplicationContext("spring-config.xml"); UserService2 userService2=context.getBean("userService2",UserService2.class); userService2.sayHi(); } } //available: expected single matching bean but found 2: user1,user2
依赖注入 vs 依赖查找
• 依赖查找依赖Bean的名称
• @Autowried 依赖注入流程:首先先根据 getType (从spring容器)获取对象,如果只获取一个,那么直接将此对象 注入到当前属性中;如果获取到多个对象,才会使用getName(根据名称)进行匹配。
此时我们发现会报错,因为找到了两个该类型的对象。此时我们可以通过以下几种方法解决:• 将属性名成改为Bean 储存在spring容器中对应的名称
@Autowired User user2; User user1;
使用注解 @Qualifier
1. @Autowired 2. @Qualifier("user2") 3. User user;
优点:使用简单
缺点:
• 无法注入final 修饰的属性
• 只适用于IoC容器
3.2 Setter 注入
@Service public class SetterUserService { private UserResposity userResposity; @Autowired public void setRepository(UserResposity repository){ this.userResposity=repository; } public void sayHi(){ userResposity.add(); } } @Test public void test() { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); SetterUserService service = context.getBean("setterUserService", SetterUserService.class); service.sayHi(); }
优点:通常Setter只Set一个属性,所以Setter注入更符合单一设计原则
缺点:
• 无法注入一个final 修饰的变量
• setter 注入的对象可以被修改,setter 本来就是一个方法,既然是一个方法,就有可能被多次调用和修改。
3.3 构造方法注入
@Service public class ConstructorService { //可以注入final修饰的属性 private final UserResposity userResposity; // private UserResposity userResposity; //如果当前类中只存在一个构造方法时,@Autowired可以省略 @Autowired //若容器中有多个UserResposity时,可以通过参数列表中的变量名改为容器中对应的名字 public ConstructorService(UserResposity userResposity) { this.userResposity = userResposity; } public void sayHi(){ userResposity.add(); } } @Test public void test() { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); ConstructorService service = context.getBean("constructorService", ConstructorService.class); service.sayHi(); }
优点:
• 可以注入一个final 修饰的变量
• 注入的对象不会被改变(构造方法只调用一次)
• 构造方法可以保证注入对象完全初始化
• 通用性更好(不依赖IoC)
四. @Resource
@Service public class UserService2 { @Resource(name = "user2") User user; public void sayHi(){ System.out.println(user.toString()); } } class UserService2Test { @Test public void test(){ ApplicationContext context= new ClassPathXmlApplicationContext("spring-config.xml"); UserService2 userService2=context.getBean("userService2",UserService2.class); userService2.sayHi(); } }
@Autowried与@Resource 的区别:
• 出身不同:@Resource 来自于JKD;@Autowired来自于Spring
• 支持参数不同:@Resource 支持的参数有很多,@Autowired只支持一个参数
• 使用上不同:@Resource 不支持构造方法注入;而@Autowired 支持
• idea 兼容性支持不同:使用@Atowired 在idea 专业版下可能误报(required 设置为false表示该属性可不被注入,可以通过设置为false来解决)
五. Bean 作用域
案例:
@Service public class ScopeService1 { @Resource(name = "user1") private User user; @Resource(name = "user1") private User user2; public void print(){ user.setId(222); user2.setId(333); System.out.println("user1:"+user.toString()); System.out.println("user2:"+user2.toString()); } } @Test public void Test(){ ApplicationContext context=new ClassPathXmlApplicationContext("spring-config.xml"); ScopeService1 service1=context.getBean("scopeService1",ScopeService1.class); service1.print(); }
我们想要两个user属性有不同的id,但是设置完后发现两个user的id相同。
原因分析:操作以上问题的原因是因为Bean 默认情况下是单例状态,也就是所有人使用的都是同一个对象。单例模式可以很大程度上提高性能,所以在Spring中Bean的作用域默认也是 singleton单例模式
解决方案:在bean对象放入Spring 容器中时,指定作用域
@Bean("user1") @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public User user1(){ User user=new User(); user.setId(1); user.setName("张三"); return user; }
5.1 作用域定义
Bean 的作用域是指 Bean 在 Spring 整个框架中的某种行为模式,比如 singleton 单例作用域,就表示 Bean 在整个 Spring 中只有一份,它是全局共享的,那么当其他人修改了这个值之后,那么另一个人读取到的就是被修改的值。
5.2 Bean 的6 中作用域
sigleton
• 描述:该作用域下的Bean在IoC 中只存在一个实例:获取Bean及装配Bean都是同一个对象
• 场景:通常无状态的Bean使用该作用域。无状态表示Bean对象的属性不需要更新
• 备注:Spring 默认选择该作用域
prototype
• 描述:每次对该作用域下的Bean 的请求都会创建新的实例:获取Bean及装配Bean都是新的对象
• 场景:通常有状态的Bean使用该作用域
request
描述:每次 http 请求都会创建新的Bean实例,类似于prototype
场景:一次 http 请求和响应的共享Bean
备注:限定 SpringMVC 中使用
session
描述:在一个 http session 中定义一个Bean实例
场景:用户会话的共享Bean,比如:记录一个用户的登录信息
备注:限定SpringMVC中使用
application
描述:在一个 http servlet Context 中,定义一个Bean实例
场景:Web 应用的上下文信息,比如:记录一个应用的共享信息
备注:限定在SpringMVC中使用
websocket
描述:在一个http websocket 的生命周期中,定义一个Bean实例
场景:WebSocket 的每次会话中,保存了一个Map 结构的头信息,将用来包裹客户端消息头。第一次初始化后,直到 WebSocket 结束都是同一个Bean
备注:限定Spring WebSocket 中使用
六. Spring 执行流程和 Bean 的生命周期
6.1 Spring 的执行流程
Spring 生命周期:
• 启动容器
• 读取配置进行 Bean 初始化
• 将 Bean 加入到容器中
• 装配 Bean 属性(给当前类的属性ID进行注入)
6.2 Bean 的生命周期
1. 实例化(内存空间分配)
2. 设置Bean 属性 (进行依赖注入,将依赖的Bean 赋值到当前类的属性上)
3. Bean的初始化
• 实现了各种 Aware 通知的方法,如 BeanNameAware、BeanFactoryAware、ApplicationContextAware 的接口方法;
• 执行 BeanPostProcessor初始化的前置方法
• 执行 @PostConsturct 初始化方法,依赖注入操作之后被执行
• 执行 BeanPostProcessor 初始化后置方法
@Component public class BeanLifeComponent implements BeanNameAware { @Override public void setBeanName(String name) { System.out.println("执行了 setBeanName 方法:"+name); } // 初始化方法 @PostConstruct public void postConsturct(){ System.out.println("执行了 postConsturct 方法"); } //这个方法是和初始化方法一样的,只是这里需要在xml文件中声明 public void myInit(){ System.out.println("执行了 myInit 方法"); } public void use(){ System.out.println("使用Bean"); } @PreDestroy public void preDestroy(){ System.out.println("执行了 preDestroy 方法"); } } public static void main(String[] args) { //这里不要使用ApplicationContext,因为它没有close方法 ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring-config.xml"); BeanLifeComponent beanLife=context.getBean("myBean",BeanLifeComponent.class); //使用Bean beanLife.use(); //此时的上下文对象若不关闭,是不会执行 preDestroy 方法的 context.close(); }
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"> <!--这里的init-method 的名字和我们代码中初始化方法名字要一致--> <bean id="myBean" class="BeanLifeComponent" init-method="myInit"></bean> </beans>
4. 使用Bean
5. 销毁Bean
销毁容器的各种方法,如@PreDestroy、DisposableBean 接口方法、destroy-method。
思考:为什么要先设置属性在进行初始化呢
因为我的初始化方法在执行的过程中可能会用到我这个类中的某个属性,如果我不先进行属性的赋值,那么在执行到使用属性的这行代码时一定会报错