2021-06-28

Springboot

如何将Bean装配到IoC容器中

1.BeanFactory:

在Spring的定义中,它要求所有的IoC容器都需要实现接口BeanFactory

按类型或者名称获取Bean
isSingleton是否单例
isPrototype是否原型

2.ApplicationContext

在这里插入图片描述
在BeanFactory的基础上,扩展了消息国际化接口(MessageSource)、环境可配置接口(EnvironmentCapable)、应用事件发布接口(ApplicationEventPublisher)和资源模式解析接口(ResourcePatternResolver),所以它的功能会更为强大。

3.基于注解的IoC容器,AnnotationConfigApplicationContext

将Java配置文件AppConfig传递给AnnotationConfigApplicationContext的构造方法,这样它就能够读取配置了。然后将配置里面的Bean装配到IoC容器中,于是可以使用getBean方法获取对应的POJO。

4.@Component和@ComponentScan。@Component是标明哪个类被扫描进入Spring IoC容器,而@ComponentScan则是标明采用何种策略去扫描装配Bean。

下面展示一些 内联代码片

//ComponentScan源码
package org.springframework.context.annotation;
/**imports**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 在一个类中可重复定义
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
// 定义扫描的包
@AliasFor("basePackages")
String[] value() default {};
// 定义扫描的包
@AliasFor("value")
String[] basePackages() default {};
   // 定义扫描的类
Class<?>[] basePackageClasses() default {};
   // Bean name生成器
   Class<? extends BeanNameGenerator> nameGenerator()
     default BeanNameGenerator.class;
   // 作用域解析器
   Class<? extends ScopeMetadataResolver> scopeResolver()
     default AnnotationScopeMetadataResolver.class;
   // 作用域代理模式
   ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
   // 资源匹配模式
   String resourcePattern() default
   
 ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
   // 是否启用默认的过滤器
   boolean useDefaultFilters() default true;
// 当满足过滤器的条件时扫描
Filter[] includeFilters() default {};
// 当不满足过滤器的条件时扫描
Filter[] excludeFilters() default {};
// 是否延迟初始化
boolean lazyInit() default false;
   // 定义过滤器
   @Retention(RetentionPolicy.RUNTIME)
   @Target({})
   @interface Filter {
     // 过滤器类型,可以按注解类型或者正则式等过滤
     FilterType type() default FilterType.ANNOTATION;
     // 定义过滤的类
     @AliasFor("classes")
     Class<?>[] value() default {};
     // 定义过滤的类
     @AliasFor("value")
     Class<?>[] classes() default {};
     // 匹配方式
     String[] pattern() default {};

   }
5.自定义第三方Bean

用@Bean注解

如何进行获取,Bean之间的依赖,在Spring IoC的概念中,我们称为依赖注入(Dependency Injection,DI)。

1.@Autowired

可以用在属性(无参构造器),方法和方法(带有参数的构造方法类)的参数上。
它注入的机制最基本的一条是根据类型(by type),我们回顾IoC容器的顶级接口BeanFactory,就可以知道IoC容器是通过getBean方法获取对应Bean的,而getBean又支持根据类型(by type)或者根据名称(by name)。

@Autowired提供这样的规则,首先它会根据类型找到对应的Bean,如果对应类型的Bean不是唯一的,那么它会根据其属性名称和Bean的名称进行匹配。如果匹配得上,就会使用该Bean;如果还无法匹配,就会抛出异常。

@Autowired是一个默认必须找到对应Bean的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为null,那么你可以配置@Autowired属性required为false,

expected single matching bean but found 2: cat,dog
2.消除歧义性

@Primary和@Quelifier
@Primary的含义告诉Spring IoC容器,当发现有多个同样类型的Bean时,请优先使用我进行注入
有时候@Primary也可以使用在多个类上,也许无论是猫还是狗都可能带上@Primary注解,其结果是IoC容器还是无法区分采用哪个Bean的实例进行注入,又或者说我们需要更加灵活的机制来实现注入,那么@Quelifier可以满足你的这个愿望。它的配置项value需要一个字符串去定义,它将与@Autowired组合在一起,通过类型和名称一起找到Bean。我们知道Bean名称在Spring IoC容器中是唯一的标识,通过这个就可以消除歧义性了。此时你是否想起了BeanFactory接口中的这个方法呢?

<T> T getBean(String name, Class<T> requiredType) throws BeansException;

通过它就能够按照名称和类型的结合找到对象了。下面假设猫已经标注了@Primary,而我们需要的是狗提供服务,因此需要修改BussinessPerson属性animal的标注以适合我们的需要,如下所示:

@Autowired

@Qualifier("dog")

private Animal animal = null;

一旦这样声明,Spring IoC将会以类型和名称去寻找对应的Bean进行注入。根据类型和名称,显然也只能找到狗为我们服务了。

3.带有参数的构造方法类的装配

有些类只有带有参数的构造方法,于是上述的方法都不能再使用了。为了满足这个功能,我们可以使用@Autowired注解对构造方法的参数进行注入。

package com.springboot.chapter3.pojo;

/******** imports ********/

@Component

public class BussinessPerson implements Person {

   private Animal animal = null;

   public BussinessPerson(@Autowired @Qualifier("dog") Animal animal) {

this.animal = animal;

}

   @Override

   public void service() {

     this.animal.use();

   }

   @Override

   public void setAnimal(Animal animal) {

     this.animal = animal;

   }

}

代码中取消了@Autowired对属性和方法的标注。注意加粗的代码,在参数上加入了@Autowired和@Qualifier注解,使得它能够注入进来。这里使用@Qualifier是为了避免歧义性。当然,如果你的环境中不是有猫有狗,则可以完全不使用@Qualifier,而单单使用@Autowired就可以了。

生命周期

Bean的生命周期的过程,它大致分为Bean定义、Bean的初始化、Bean的生存期和Bean的销毁4个部分。

Bean定义
  • Spring通过我们的配置,如@ComponentScan定义的扫描路径去找到带有@Component的类,这个过程就是一个资源定位的过程。

  • 一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始化Bean,也就没有Bean的实例,它有的仅仅是Bean的定义。

  • 然后就会把Bean定义发布到Spring IoC容器中。此时,IoC容器也只有Bean的定义,还是没有Bean的实例生成。

spring在这里插入图片描述
ComponentScan中还有一个配置项lazyInit,只可以配置Boolean值,且默认值为false,也就是默认不进行延迟初始化,因此在默认的情况下Spring会对Bean进行实例化和依赖注入对应的属性值。
在这里插入图片描述

如图,在没有注释的情况下的流程节点都是针对单个Bean而言的,但是BeanPostProcessor是针对所有Bean而言的,这是我们需要注意的地方。

即使你定义了ApplicationContextAware接口,但是有时候并不会调用,这要根据你的IoC容器来决定。我们知道,Spring IoC容器最低的要求是实现BeanFactory接口,而不是实现ApplicationContext接口。对于那些没有实现ApplicationContext接口的容器,在生命周期对应的ApplicationContextAware定义的方法也是不会被调用的,只有实现了ApplicationContext接口的容器,才会在生命周期调用ApplicationContextAware所定义的setApplicationContext方法。

使用属性文件

在Spring Boot中,我们先在Maven配置文件中加载依赖

<dependency>

   <groupId>org.springframework.boot</groupId>

   <artifactId>spring-boot-configuration-processor</artifactId>

   <optional>true</optional>

</dependency>

有了依赖,就可以直接使用application.properties文件为你工作了

配置属性

database.driverName=com.mysql.jdbc.Driver

database.url=jdbc:mysql://localhost:3306/chapter3

database.username=root

database.password=123456

会通过其机制读取到上下文中,这样可以引用它了。对于它的引用,有两种方法,首先是用Spring表达式。本节我们只限于读取属性而不涉及运算。关于其运算,后面再谈及

使用属性配置


@Value("${database.driverName}")

   private String driverName = null;
   
 @Value("${database.username}")

   public void setUsername(String username) {

     System.out.println(username);

     this.username = username;

   }
  • 这样我们就可以通过@Value注解,使用${…}这样的占位符读取配置在属性文件的内容。这里的@Value注解,既可以加载属性,也可以加在方法上。

  • value可以配置多个配置文件。使用classpath前缀,意味着去类文件路径下找到属性文件;ignoreResourceNotFound则是是否忽略配置文件找不到的问题。ignoreResourceNotFound的默认值为false,也就是没有找到属性文件,就会报错;这里配置为true,也就是找不到就忽略掉,不会报错。

  • 也可以使用注解@ConfigurationProperties,通过它使得配置上有所减少

@Component

@ConfigurationProperties("database")

public class DataBaseProperties {

}

这里在注解@ConfigurationProperties中配置的字符串database,将与POJO的属性名称组成属性的全限定名去配置文件里查找,这样就能将对应的属性读入到POJO当中。

有时候我们会觉得如果把所有的内容都配置到application.properties,显然这个文件将有很多内容。为了更好地配置,我们可以选择使用新的属性文件。例如,数据库的属性可以配置在jdbc.properties中,于是先把代码清单3-22中给出的配置从application.properties中迁移到jdbc.properties中,然后使用@PropertySource去定义对应的属性文件,把它加载到Spring的上下文中,如下:

使用载入属性文件

package com.springboot.chapter3.main;

/******** imports ********/

@SpringBootApplication

@ComponentScan(basePackages = {"com.springboot.chapter3"})

@PropertySource(value={"classpath:jdbc.properties"}, ignoreResourceNotFound=true)

public class Chapter3Application {

   public static void main(String[] args) {

     SpringApplication.run(Chapter3Application.class, args);

   }

}

条件装配Bean

@Conditional注解帮助我们,而它需要配合另外一个接口Condition(org.springframework.context.annotation.Condition)来完成对应的功能。

如果一个类加入了@Conditional注解,并且配置了类DatabaseConditional,那么这个类就必须实现Condition接口。对于Condition接口则要求实现matches方法。

Bean的作用域

在一般的容器中,Bean都会存在单例(Singleton)和原型(Prototype)两种作用域,Java EE广泛地使用在互联网中,而在Web容器中,则存在页面(page)、请求(request)、会话(session)和应用(application)4种作用域。对于页面(page),是针对JSP当前页面的作用域,所以Spring是无法支持的。为了满足各类的作用域,在Spring的作用域中就存在如表3-1所示的几种类型。
在这里插入图片描述

使用@Profile

Spring还提供了Profile机制,使我们可以很方便地实现各个环境之间的切换。

在Spring中存在两个参数可以提供给我们配置,以修改启动Profile机制,一个是spring.profiles.active,另一个是spring.profiles.default。在这两个属性都没有配置的情况下,Spring将不会启动Profile机制,这就意味着被@Profile标注的Bean将不会被Spring装配到IoC容器中。Spring是先判定是否存在spring.profiles.active配置后,再去查找spring.profiles.default配置的,所以spring.profiles.active的优先级要大于spring.profiles.default。

引入XML配置Bean

我们也可以在Spring Boot中使用XML对Bean进行配置。这里需要使用的是注解@ImportResource,通过它可以引入对应的XML文件,用以加载Bean。有时候有些框架(如Dubbo)是基于Spring的XML方式进行开发的,这个时候需要引入XML的方式来实现配置。

使用Spring EL

Spring还提供了表达式语言Spring EL。通过Spring EL可以拥有更为强大的运算规则来更好地装配Bean。

最常用的当然是读取属性文件的值,例如:

@Value("${database.driverName}")

String driver

除此之外,它还能够调用方法,例如,我们记录一个Bean的初始化时间:

@Value("#{T(System).currentTimeMillis()}")

private Long initTime = null;

注意,这里采用#{…}代表启用Spring表达式,它将具有运算的功能;T(…)代表的是引入类;System是java.lang.*包的类,这是Java默认加载的包,因此可以不必写全限定名,如果是其他的包,则需要写出全限定名才能引用类;currentTimeMillis是它的静态(static)方法,也就是我们调用一次System.currentTimeMillis()方法来为这个属性赋值。

//使用Spring EL赋值

// 赋值字符串

@Value("#{'使用Spring EL赋值字符串'}")

private String str = null;

// 科学计数法赋值

@Value("#{9.3E3}")

private double d;

// 赋值浮点数

@Value("#{3.14}")

private float pi;
使用Spring EL进行计算

#数学运算

@Value("#{1+2}")

private int run;

#浮点数比较运算

@Value("#{beanName.pi == 3.14f}")

private boolean piFlag;

#字符串比较运算

@Value("#{beanName.str eq 'Spring Boot'}")

private boolean strFlag;

#字符串连接

@Value("#{beanName.str + ' 连接字符串'}")

private String strApp = null;

#三元运算

@Value("#{beanName.d > 1000 ? '大于' : '小于'}")

private String resultDesc = null;
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个可能的Java实现: ```java import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; public class RentPlanGenerator { private static final double RENT_INCREASE_RATE = 0.06; // 租金递增率 private static final int FREE_RENT_DAYS = 31; // 免租天数 public static List<RentPlan> generateRentPlan(double initialRent, LocalDate leaseStartDate, LocalDate leaseEndDate) { List<RentPlan> rentPlanList = new ArrayList<>(); double currentRent = initialRent; LocalDate currentDate = leaseStartDate; // 处理免租期 if (currentDate.isBefore(leaseStartDate.plusDays(FREE_RENT_DAYS))) { currentDate = leaseStartDate.plusDays(FREE_RENT_DAYS); } while (currentDate.isBefore(leaseEndDate)) { LocalDate nextIncreaseDate = currentDate.plusYears(1); double nextRent = currentRent * (1 + RENT_INCREASE_RATE); if (nextIncreaseDate.isBefore(leaseStartDate.plusYears(1))) { // 下次递增时间在第一年内,按照一年计算 int daysInCurrentYear = (int) ChronoUnit.DAYS.between(currentDate, nextIncreaseDate); rentPlanList.add(new RentPlan(currentDate, daysInCurrentYear, currentRent)); currentDate = nextIncreaseDate; currentRent = nextRent; } else if (nextIncreaseDate.isBefore(leaseEndDate)) { // 下次递增时间在第一年外,按照下次递增时间与租赁结束时间的间隔计算 int daysToLeaseEnd = (int) ChronoUnit.DAYS.between(currentDate, leaseEndDate); rentPlanList.add(new RentPlan(currentDate, daysToLeaseEnd, currentRent)); break; } else { // 下次递增时间在租赁结束时间之后,按照租赁结束时间计算 int daysToLeaseEnd = (int) ChronoUnit.DAYS.between(currentDate, leaseEndDate); rentPlanList.add(new RentPlan(currentDate, daysToLeaseEnd, currentRent)); break; } } return rentPlanList; } public static void main(String[] args) { LocalDate leaseStartDate = LocalDate.of(2021, 3, 1); LocalDate leaseEndDate = LocalDate.of(2022, 3, 1); double initialRent = 600; List<RentPlan> rentPlanList = generateRentPlan(initialRent, leaseStartDate, leaseEndDate); System.out.printf("%-12s%-12s%-12s%n", "时间", "天数", "租金"); for (RentPlan rentPlan : rentPlanList) { System.out.printf("%-12s%-12d%-12.2f%n", rentPlan.getStartDate(), rentPlan.getDays(), rentPlan.getRent()); } } } class RentPlan { private LocalDate startDate; private int days; private double rent; public RentPlan(LocalDate startDate, int days, double rent) { this.startDate = startDate; this.days = days; this.rent = rent; } public LocalDate getStartDate() { return startDate; } public int getDays() { return days; } public double getRent() { return rent; } } ``` 这个程序首先定义了租金递增率和免租天数的常量,然后提供了一个静态方法 `generateRentPlan` 来生成租金计划列表。该方法接受三个参数:初始月租金、租赁开始时间和租赁结束时间。 具体实现时,我们使用循环来逐月生成租金计划。在每次循环中,我们首先计算下次递增租金的时间和金额。然后根据下次递增时间与租赁开始时间的间隔,决定本次循环处理的天数和租金金额。最后将这些信息保存到一个 `RentPlan` 对象中,并添加到租金计划列表中。 在主函数中,我们使用 `generateRentPlan` 方法生成租金计划列表,并以表格形式输出。输出结果如下: ``` 时间 天数 租金 2021-04-01 30 600.00 2021-05-01 31 636.00 2021-06-01 30 674.16 2021-07-01 31 713.57 2021-08-01 31 754.29 2021-09-01 30 796.39 2021-10-01 31 840.94 2021-11-01 30 887.02 2021-12-01 31 934.72 2022-01-01 31 984.12 2022-02-01 28 1035.30 ``` 可以看到,程序正确地根据递增周期和递增率生成了每个月的租金计划,并且考虑了免租期的影响。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值