Spring
丝不润是一个非常庞大的框架,真的是太蓝了。下面就很菜鸡的记录一下。
Spring特性之IoC组件添加
具体啥叫Ioc控制反转,去康康其他博客。
首先,控制反转的思想就是要将对象交给Spring容器来托管。容器托管的方式也有很多,比如用xml配置文件,注解。后面使用SpringBoot将大量的使用到注解的方式,下面就举个栗子来说说注解驱动的Spring。
要使用Spring,首先得有Spring工程。使用Maven管理工具就很方便。
不是很专业啊,先在Maven工程里边建一个目录结构,bean中放置对象的类。在springConfig包中放置Spring的配置类。IoCTest就是放用于测试的类。
- 001.给IoC容器添加组件之-> 使用@Bean
重点在于配置类(MainConfiguration)上,配置类类似于之前使用的xml文件。
package springConfig;
import bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //用来说明该类是一个配置类
public class MainConfiguration {
@Bean //说明下面的方法是会注册bean
public Person person(){
return new Person("zhangsan",29);
}
}
之后在测试类中进行测试。首先需要注册容器对象(不晓得这么说准不准确),之后通过容器对象获取bean
package IoCTest;
import bean.Person;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import springConfig.MainConfiguration;
public class Test1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext =
new AnnotationConfigApplicationContext(MainConfiguration.class);
Person person = (Person)annotationConfigApplicationContext.getBean("person");
System.out.println(person);
}
}
使用,就是这么使用,看起来贼简单。但是!当铺天盖地的属性配置注解袭来,还是会搞得眼花缭乱。
-
- (xml的id怎么注解实现?)上面测试类中,通过容器getBean()中间输入的是配置类中的方法名。在配置类中的Bean注解中可以设置值。如@Bean(“hehe”),这就相当于在xml文件中的Bean标签中的id。此时在getBean的时候就应该getBean(“hehe”)。
-
- (获取BeanName)annotationConfigApplicationContext 中有个功能是getBeanNamesForType,可以将某一个Bean类的所有bean的名字保存为一个String数组。如果Bean中有指定“id”那么,将优先保存Bean id,否则保存的是Bean方法名。
String[] name = annotationConfigApplicationContext.getBeanNamesForType(Person.class);
for (String s : name) {
System.out.println(s);
}
所以说到getBean,肯定会发现,getBean不仅仅可以通过id来获取,还能够通过组件名来获取,也就是通过组件的类对象来获取。比如:
Student student = annotationConfigApplicationContext.getBean(Student.class);
所以 getBean(id) 和 getBean(Class<Object>) 有啥子区别喃?
其实也很好理解,getBean(id) 就是精准打击,指哪打哪。
如果是getBean(Class<Object>) 的话就属于蒙眼抓虾,抓的是容器中Object类的Bean,那抓的是哪个Bean呢?这个问题就限制了Bean的个数,容器中这个类的Bean只能有1个或者0个(说0个不是很准确),蒙眼抓的话就只抓那一个。如果容器里存在两个Bean,那就会抛出异常,莫法接着快乐的玩耍了。
< Bean 的单实例多实例问题>
Bean被加载进容器,还有一个问题就是单实例或者多实例。
默认情况下,Bean是单实例的。例如,上述两次getBean,他们拿到的是同一个实例对象。这个单实例和传统意义上的单实例还是有一定的区别的,传统意义上的单实例是指某一个类的对象只能有一个,比方说人类是单实例,那就只允许有一个人,多实例就是可以有很多人。Bean的单实例是指,Bean对象是能有一个,比方说小明这个人只有能有一个小明,多实例就可以有很多小明的克隆体。(形容不当,将就理解一下)
Person person1 = (Person) annotationConfigApplicationContext.getBean("hehe");
Person person2 = (Person) annotationConfigApplicationContext.getBean("hehe");
System.out.println(person1 == person2);
上述输出结果为true。
如何设置为多实例呢?(使用@Scope)
@Scope有4种可输入属性:1.prototype 2.singleton 3.request 4.session。3,4先不说了。1就是多实例,2就是单实例(默认)。因此,在hehe方法的上面添加一个@Scope(“prototype”),上述的代码将返回false。
< Bean 的加载时机问题>
加载时机,说白了也就是调用被@Bean修饰的方法被调用的时机。
首先先说结论:
容器创建时 | getBean时 | |
---|---|---|
prototype | 不会被加载 | 每一次get就加载一次 |
singleton | 会被加载进容器 | 后面每一次get都不会加载 |
其实也很好理解,因为多实例的模式下,每一次getBean都是要得到一个新的对象。因此每一次get都需要去调用方法来new 一个 Person的对象。所以刚刚创建容器的时候也没必要去加载,等需要了就加载去new一个。单实例的模式下,容器刚刚创建,就调用方法new一个对象放在容器当中,等需要get的时候就直接将创建好的对象拿出来。每一次getBean的时候拿到的都是同一个对象。拿同一个对象当然不需要去new,因此也就不会去加载。
如果说在单实例的模式下,不想要再容器已创建就加载咋整?(@Lazy)
那就在方法上添加一个@lazy注解,实现懒加载模式。只有再第一次被getBean的时候才会去加载。
< Bean 的加载条件:@Conditional注解>
如果说在有的情况下,我们需要加载Bean进容器,但有的情况下我们不想加进容器,这时候就需要用到@Conditional注解了。
点进Conditional注解可以看到,参数位置需要输入的是Class类,类还得要实现Condition接口。
public @interface Conditional {
Class<? extends Condition>[] value();
}
假如需要实现一个需求,写两个Bean,一个为Bill对象,一个为Linux对象。当系统为Windows系统时就只加载Bill对象,当为Linux系统时就只加载Linux对象。
@Bean("Windows")
@Conditional(WindowsCondition.class)
public Person person02() { return new Person("Bill",59); }
@Bean("Linux")
@Conditional(LinuxCondition.class)
public Person person03() { return new Person("Linux",39);}
刚刚已经说过,Conditional注解中传入的为实现Condition接口的Class类,因此需要写两个类来实现Condition接口。上面的@Conditional注解已经写好了。
第一个类:WindowsCondition:
package com.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class WindowsCondition implements Condition {
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment(); //获取运行环境
String osName = environment.getProperty("os.name"); //获取当前系统名称
return osName.contains("Windows"); //如果名称中包含Windows则返回true
}
}
第二个类:LinuxCondition:
package com.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class LinuxCondition implements Condition {
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();//获取运行环境
String osName = environment.getProperty("os.name");//获取当前系统名称
return osName.contains("Linux");//如果名称中包含Linux则返回true
}
}
可以看到实现Condition接口的matches方法,当返回为true时,注解修饰的Bean就会被加载,反之亦然。
@Conditional注解可以作用于方法之上,也可以作用于类上。
假如吧带有LinuxCondition 的Conditional注解注释在MainConfiguration的类头上,则容器中不会加载任何组件。
- 002.给IoC容器添加组件之-> 包扫描@ComponentScan
包扫描注解就是将注解指定包下被@Component @Controller @Service @Repository标识的类添加到IoC容器。
设定以下结构目录:分别给其中的类头上添加注解(图中红色@所示)
在MainConfiguration类头中添加@ComponentScan(“包名”)
测试中还是使用容器对象的getBeanDefinitionNames(),返回一个String[] 。这个方法会将容器中的组件名返回。
结果如下图:
如果没有添加@ComponentScan注解,则只会出现@Configuration和@Bean注释的组件。
- 003.给IoC容器添加组件之-> 导入组件@Import
相比较包扫描@ComponentScan注解,@Import就显得更精准些。
假如在bean包中定义了其他几个类,想要在再容器创建时加载到容器当中,就得在配置类的类头上添加@Import注解。注解中传入的是需要加载进容器的类对象(Xxx.class),就是这么easy。
@Import({Student.class, Teacher.class,Worker.class})
如果一个一个的写不是很方便,可以使用ImportSelector类来进行管理。
首先,定义一个类实现ImportSelector接口,实现selectImports方法。方法的返回值为一个String[]数组,将需要加载进容器的类的全类名一一写进数组就可以了。
然后在@Import的地方只需要传入ImportSelector的实现类的类对象就ok。
ImportSelector的实现类:
package com.condition;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[] {"com.bean.Student","com.bean.Teacher","com.bean.Worker"};
}
}
@import:
@Import({ MyImportSelector.class})
直接使用Import进类对象和使用ImportSelector达到的效果是一样的。对了,这样被加载进容器的组件是单例的。
现在已经说了两种import的方式,下面害有一种:ImportBeanDefinitionRegistrar接口
ImportBeanDefinitionRegistrar接口和ImportSelector的用法是差不多的,都是需要实现这个接口,重写接口方法。然后将实现类的类对象放进Import中。
ImportBeanDefinitionRegistrar的重写方法如下:
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
//AnnotationMetadata个人理解是Import注解位置的所有注解信息
//BeanDefinitionRegistry是对容器bean注册注销等等功能的对象
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
//发现下面需要一个BeanDefinition对象,就看看它的实现类,RootBeanDefinition
//构造函数传入需要注册组件的类对象就阔以了
RootBeanDefinition rootBeanDefinitionDoc = new RootBeanDefinition(Doctor.class);
//通过beanDefinitionRegistry的registerBeanDefinition方法注册Bean。传入参数为Bean id和Bean的Definition对象↑
beanDefinitionRegistry.registerBeanDefinition("doc1",rootBeanDefinitionDoc);
}
}
就按上述代码,容器中就有了一个id为doc1的Bean。
- 004.给IoC容器添加组件之-> 实现FactoryBean接口
通过Bean工厂来获得bean。步骤如下:
- 实现FactoryBean接口
- 重写接口方法
- 将实现类以@Bean的形式假如配置类
- 大功告成
首先实现FactoryBean接口,注意不是BeanFactory接口啊!注意不是BeanFactory接口啊!注意不是BeanFactory接口啊!
以建Person类的Bean为例:
package com.bean;
import org.springframework.beans.factory.FactoryBean;
public class PersonFactory implements FactoryBean<Person> {
public Person getObject() throws Exception {
return new Person(); //返回一个Person对象
}
public Class<?> getObjectType() {
return Person.class; //返回Person类的类对象
}
public boolean isSingleton() {
return false; //如果想要这个Bean是个单例的那就改成true
}
}
之后在Config类中添加实现类的Bean
@Bean
public PersonFactory personFactory1(){return new PersonFactory();}
注意看这里的方法返回对象是一个PersonFactory类的对象。一会getBean有惊喜…
好这里来getBean,并获取Bean的类对象的名称
Object personFactory1 = annotationConfigApplicationContext.getBean("personFactory1");
System.out.println(personFactory1.getClass().getName());
结果是:com.bean.Person
居然不是PersonFactory ,这个就是FactoryBean的神奇之处,它把内部方法封装的类对象放入了容器当中。那如果要想获得工厂Bean的Bean咋整?(咋那么拗口)那就在getBean的id前面加一个&
因为,在BeanFactory接口中定义了一个前缀符,用于获取工厂对象本身。(这个地方又成了BeanFactory,具体为啥能work,我也晓不得)
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
至此,4中添加Bean的方式已经介绍完了。
Bean 的生命周期
bean的生命周期也就是:创建->初始化->销毁
管理其生命周期也是有4中方法,下面来说说
- 001. @Bean
在一个类中定义好初始化方法和销毁方法:
public class Driver extends Person {
public Driver() { }
public Driver(String name, int age) { super(name, age); }
public void init(){
System.out.println("Driver's init method is running...");
}
public void destory(){
System.out.println("Driver's destory method is running...");
}
}
在配置类中一样定义一个Bean,Bean中就可以指定初始化方法和销毁方法。
@Bean(initMethod = "init",destroyMethod = "destory")
public Driver driver(){ return new Driver("阿富",19); }
接下来创建容器,发现init方法被执行了。然后关闭容器:
annotationConfigApplicationContext.close();
destory方法也会被执行。
这是在默认单例的情况, 如果将@scope设置为prototype结果将不一样
多例情况之前也说过,只有再getBean的时候才会加载bean。所以只创建容器的话是不会去执行init方法的,init方法只有再getBean的时候才会执行,没get一次就执行一次。
特殊的一点是,在销毁容器的时候,destory方法是不会被执行的。
- 002. 实现InitializingBean, DisposableBean接口
使用这种方式,实现InitializingBean接口,重写afterPropertiesSet()方法,执行初始化工作。实现 DisposableBean接口,重写destroy()方法,执行销毁工作。这样就不需要去在@Bean中指定初始化方法和销毁方法了。下面举个栗子:
@Component
public class Client extends Person implements InitializingBean, DisposableBean {
public Client() { System.out.println("Client's constructor..."); }
public Client(String name, int age) {
super(name, age);
System.out.println("Client's constructor with parameter...");
}
public void destroy() throws Exception {
System.out.println("Client's destory method...");
}
public void afterPropertiesSet() throws Exception {
System.out.println("Client's init method...");
}
}
- 003. @PostConstruct、@PreDestroy注解
这两个注解其实光看名字就能猜到大概怎么用了,PostConstruct后于构造器,这很明显就是用来修饰初始化方法的。PreDestory预销毁,这个就是销毁方法。多的不说先,看栗子:
@Component
public class Cooker extends Person {
public Cooker(){ System.out.println("Cooker's constructor..."); }
@PostConstruct
public void init(){ System.out.println("Cooker's init method..."); }
@PreDestroy
public void destory(){ System.out.println("Cooker's destory method..."); }
}
- 004. 初始化的前后置通知BeanPostProcessor接口
这个就是一个初始化方法的前后置通知,实现这个接口,重写两个方法。直接看栗子吧:
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
//在Bean的初始化之前执行(前置通知)
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println(beanName+"postBeforeInit...");
return null;
}
//在Bean的初始化之后执行(后置通知)
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println(beanName+"postAfterInit...");
return null;
}
}
容器中的所有bean的加载都会被初始化,因此上面的两个方法都会被执行。对于以上三中方法实现的初始化都能够被BeanPostProcessor识别,实现前后置通知。
下面给看一个结果:
Spring特性之IoC组件赋值
组件加载到容器中,就涉及到一个问题就是注入问题。和xml中的properties的value赋值相类似。使用@Value注解也能够达到相同的效果。
@value()中可以有三种方式注入:
- 普通类型的注入,如:@Value(“123”)、@Value(“coco”)、@Value(“false”)等
- spEL表达式,如@Value(#{“5-3*3”})、@Value(#{“Car.weight”})等
- 引入properties配置文件属性,如:@Value(${Person.nickName})
@Value的位置是卸载组件类的属性上面的。上面所述的1,2方法直接写就好了。第三种方式需要用到properties配置文件,配置文件放于resources目录下就好。之后在配置类的类头要导入配置文件。如下图所示:
配置类头添加配置文件注解:
@PropertySource({"BeanProp/Person.properties"})
配置文件定义一个属性:
Person.nickName = 刘德华
之后就可以自Person类中进行属性注入了:
public class Person {
@Value("小刘")
private String name;
@Value("#{3*3}")
private int age;
@Value("${Person.nickName}")
private String nickName;
...
...
}
getBean(person),发现Bean的属性被赋了值:
<插一个题外话>
properties文件被加载之后,配置文件中的kv对也就被加进了环境变量。可以通过环境对象来获得属性值。
ConfigurableEnvironment environment = annotationConfigApplicationContext.getEnvironment();
String property = environment.getProperty("Person.nickName");
System.out.println(property)
显示结果:刘德华