01. Spring 如何根据注解创建容器?
我们都知道Spring提供了根据注解和xml文件两种方式来创建容器和管理bean的,而在此我们将使用Spring提供的注解创建出容器,并从容器中获取到bean对象。
1. 创建配置类MySpringContext.java,类上添加Spring提供的ComponentScan注解生命扫描包的路径
@ComponentScan({"com.it120"})
public class MySpringContext {
public MySpringContext(){
System.out.println("容器初始化中。。。。");
}
}
注解中的“com.it120”,表示Spring应该把该路径下贴上@Component注解的类加载到容器中
2. 在需要被Spring容器加载的类上贴上@Component注解:
@Component
public class MyBean {
public void test(){
System.out.println("执行test方法");
}
}
以上代码中我们定义了一个MyBean类,并提供了test()方法,类上我们贴上了@Component注解,表示该类将会被加载到Spring容器中
3. 根据配置类创建一个容器,并根据名称获取某个bean:
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MySpringContext.class);
MyBean myBean = (MyBean) context.getBean("myBean");
myBean.test();
}
以上代码中我们使用了Spring提供的AnnotationConfigApplicationContext类创建了一个容器上下文对象,入参为配置类的Class对象,通过容器上下文getBean(String beanNaem)方法 获取到我们加载到Spring容器中的bean对象,强转之后再调用test()方法,运行结果如图示:
以上就是根据Spring提供的注解和方法创建的容器和从容器中获取Bean的简单案例,我们暂且不深究其中奥妙,因为我们将会通过自己的创建的注解来实现以上的案例。
02. 创建容器类和自定义组件
创建容器类和自定注解
上一篇中我们使用了Spring提供的AnnotationConfigApplicationContext类来创建了一个容器上下文对象,入参为配置类的Class文件对象。并且该容器上下文对象提供了一个getBean(String beanName)的方法
那么我们可以简化思考为 AnnotationConfigApplicationContext类其实就是一个拥有Class类型成员变量和一个参数的构造器再加上一个getBean()方法的类,我们可以依此创建出容器类如下:
public class MyApplicationContext {
// Class类型的成员变量
public Class clazz;
// 构造方法
public MyApplicationContext(Class clazz){
this.clazz=clazz;
}
// getBean方法,根据名称获取一个bean对象
public Object getBean(String name){
// 先返回null,后续代码补上
return null;
}
}
创建自定义@ComponentScan()和@Component注解,这两个注解里面都拥有一个String类型的属性,前者中的属性表示包扫描的路径,后者的属性代表某个bean在容器中的名称
@ComponentScan()实现:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
String value();
}
@Component实现:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
注解中的@Target(ElementType.TYPE)表示这个注解可以使用在 类、接口上,@Retention(RetentionPolicy.RUNTIME)表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
定义一个配置类,类上使用@ComponentScan()注解,并指定包扫描路径:
@ComponentScan("com.spring_container.service")
public class AppConfig {
}
再定义一个将被加载到容器中的类,类上使用@Component注解,指定该bean在容器中的名称:
@Component("myService")
public class MyService {
}
到此我们已经把基本的类结构和注解定义完成可以在main方法中进行一个“假容器”的创建了如下:
public static void main(String[] args) throws ClassNotFoundException {
//手写实现Spring容器
MyApplicationContext myApplicationContext= new MyApplicationContext(AppConfig.class);
System.out.println(myApplicationContext.getBean("myService"));
}
但是现在我们运行其实也不会返回什么,因为我们还没完成bean对象的创建,所以这是个“空壳容器”
03. 解析注解获取包扫描路径
在上文中我们通过自定义注解和自定义容器类搭建了一个“空壳容器”,在本篇内容我们将逐步完成包扫描的过程。包扫描的流程大致可分为如下步骤:
-
解析注解获取注解的属性值
-
根据注解属性值,获取该路径下所有文件
-
通过ClassLoader 加载.class文件
包扫描和bean对象的创建都是需要在容器类中的构造方法进行创建处理的,我们可以把包扫描的步骤定义在一个方法内 名为scan(Class clazz) 之后在构造器中调用此方法即可:构造方法如下:
// 构造方法
public MyApplicationContext(Class aClass) throws ClassNotFoundException {
this.aClass=aClass;
// 扫描路径-
scan(aClass);
}
scan方法:
private void scan(Class aClass) {
//扫描包的逻辑代码
}
在Scan方法中我们第一步需要获取到注解中的扫描路径:在scan方法中添加如下代码:
//1.获取传入的配置类上的@ComponentScan里面的参数,包的扫描路径
ComponentScan componentScan = (ComponentScan)aClass.getDeclaredAnnotation(ComponentScan.class);
String path = componentScan.value();
System.out.println(path);
输出的包扫描路径如图:
获取到包扫描路径后,需要根据该路径获取到该路径下所有的文件
//1.获取传入的配置类上的@ComponentScan里面的参数,包的扫描路径
ComponentScan componentScan = (ComponentScan)aClass.getDeclaredAnnotation(ComponentScan.class);
String path = componentScan.value();
ClassLoader classLoader = MyApplicationContext.class.getClassLoader();
// 获取path下所有资源
URL resource = classLoader.getResource(path.replace(".", "/"));
// 获取文件
File file = new File(resource.getFile());
if(file.isDirectory()){
// 如果是文件夹
File[] files = file.listFiles();
for (File f: files) {
// 输出每一个文件的地址
System.out.println(f.getAbsolutePath());
}
}
运行结果如图所示:
三种类加载器
-
启动类加载器(Bootstrap classLoader),加载的是jre/lib下的文件
-
拓展类加载器(Extension classLoader),加载的是/jre/ext/lib下的文件
-
应用类加载器(appclassloader)这个加载器就是加载用户所自定义的类的,加载的是classpath路径下的文件,那classpath路经指的是哪?看下图
我们从idea的启动参数log中看到有一个Classpath对应的参数,而这里的classpath指的是相对于Target/classes/下的文件,所以我们的appclassloader将会加载classes/下面的所有文件
第三步根据包扫描路径下所有.class文件生成Class对象,这里分两个小步,第一步获取类的全限定类名,第二步通过全限定类名生成Class对象。
for (File f: files) {
if(f.getAbsolutePath().endsWith(".class")){
String absolutePath = f.getAbsolutePath();
String filePath = absolutePath.substring(absolutePath.indexOf("com"), absolutePath.indexOf(".class"));
String className = filePath.replace("\\", ".");
System.out.println(className);
}
}
// 通过类加载器,加载类
Class<?> clazz = classLoader.loadClass(className);
System.out.println(clazz);
在上文中我们通过获取注解属性值,并通过该值加载.calss文件,最终通过类加载器获取到了Class对象,获取到类的Class对象之后我们就可以通过反射来创建出类的对象了
04. 单例池和BeanDefinition对象
在上文中我们通过获取注解属性值,并通过该值加载.calss文件,最终通过类加载器获取到了Class对象,获取到类的Class对象之后我们就可以通过反射来创建出类的对象了
我们都知道Spirng中Bean的作用域有以下几种:
-
原型(prototype):每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态
-
单例(singleton):Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是Spring 中的缺省作用域,也可以显示的将 Bean 定义为 singleton 模式
-
request:在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean实例也将会被销毁
-
session:在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求内有效,请求结束,则实例将被销毁
-
global Session:在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在使用 portlet context 时有效
/**
* 单例模式
*/
public class Singleton {
private static Singleton singleton;
// 声明为私有之后在其他内就无法使用 该构造器来创建新的对象
private Singleton(){}
//只提供唯一一个访问此对象的方法
public static Singleton getSingleton(){
if(singleton==null){
return new Singleton();
}
return singleton;
}
}
//调用
public static void main(String[] args){
System.out.println(Singleton.getSingleton());
System.out.println(Singleton.getSingleton());
System.out.println(Singleton.getSingleton());
}
public static Singleton getSingleton(){
return new Singleton();
}
//bean定义对象
public class BeanDefinition {
// 类型
private Class clazz;
// bean的作用域
private String scope;
}
-
判断某个bean是否为单例类型,如果为单例类型则将该bean对象存入到单例池中
-
在扫描包的过程中将生成bean的定义对象,将bean定义对象存入到一个ConcurrentHashMap中,以供后续创建bean和获取bean时通过bean名称获取Class对象
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {
String value();
}
//单例池
private ConcurrentHashMap<String,Object> singletonMap =new ConcurrentHashMap<>();
// BeanDefinition
private ConcurrentHashMap<String,Object> beanDefinitionMap =new ConcurrentHashMap<>();
// 通过类加载器,加载类
Class<?> clazz = classLoader.loadClass(className);
if(clazz.isAnnotationPresent(Component.class)){
// 如果该类上存在@Component注解
Component component =clazz.getDeclaredAnnotation(Component.class);
// 定义一个beanDefinition对象
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClazz(clazz);
if(clazz.isAnnotationPresent(Scope.class) ){
//该类上有@Scope注解注释
beanDefinition.setScope(clazz.getDeclaredAnnotation(Scope.class).value());
}else{
//默认单例bean
beanDefinition.setScope("singleton");
}
String beanName = component.value();
beanDefinitionMap.put(beanName,beanDefinition);
}
// 构造方法
public MyApplicationContext(Class aClass) throws ClassNotFoundException {
this.aClass=aClass;
// 扫描路径----->beanDefinition---->beanDefinitionMap
scan(aClass);
// 单例bean处理
for (Map.Entry<String, Object> entry : beanDefinitionMap.entrySet()) {
String key = entry.getKey();
BeanDefinition beanDefinition = (BeanDefinition)entry.getValue();
if(beanDefinition.getScope().equals("singleton")){
Object o= createBean(beanDefinition);
singletonMap.put(key,o);
}
}
}
// getBean方法,根据名称获取一个bean对象
public Object getBean(String name){
// 根据bean名称,获取bean定义
if(beanDefinitionMap.containsKey(name)){
BeanDefinition beanDefinition =(BeanDefinition) beanDefinitionMap.get(name);
if(beanDefinition.getScope().equals("singleton")){
// 从单例池中获取
return singletonMap.get(name);
}else {
// 创建bean
return createBean(beanDefinition);
}
}else{
// 不存在对应的bean
throw new NullPointerException();
}
}
private Object createBean(BeanDefinition beanDefinition) { try { // 根据beanDefinition对象创建bean对象 Class clazz = beanDefinition.getClazz(); //通过反射生成对象 return clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); return null; } }
@Component("myService")@Scope("prototype")public class MyService {}
@Component("xxxService")public class xxxService {}
05. 简易版@Autowired依赖注入实现
在上文中我们主要讲解了bean的单例和原型作用域的区别以及单例池和BeanDefinition对象的作用和使用,将扫描路径下的bean的定义,存入到了map中,也将作用域为单例的bean存入了单例池中。
@Target({ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
@Component("orderService")
public class OrderService {
}
@Component("myService")
@Scope("prototype")
public class MyService {
@Autowired
private OrderService orderService;
public OrderService getOrderService() {
return orderService;
}
}
public class MainTest {
public static void main(String[] args) throws ClassNotFoundException {
//手写实现Spring容器
MyApplicationContext myApplicationContext= new MyApplicationContext(AppConfig.class);
MyService myService = (MyService) myApplicationContext.getBean("myService");
System.out.println(myService.getOrderService());
}
}
private Object createBean(BeanDefinition beanDefinition) {
try {
// 根据beanDefinition对象创建bean对象
Class clazz = beanDefinition.getClazz();
Object instance = clazz.newInstance();
//获取类的所有成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field: fields) {
// 如果该成员变量被@Autowired注解标识
if(field.isAnnotationPresent(Autowired.class)){
// 成员变量名称
String name = field.getName();
// 根据名称获取bean
Object bean = getBean(name);
field.setAccessible(true);
// 将获取到的bean设置到类对象中
field.set(instance,bean);
}
}
return instance;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
BeanNameAware接口
public interface BeanNameAware {
void setBeanName(String name);
}
@Component("myService")
@Scope("prototype")
public class MyService implements BeanNameAware {
@Autowired
private OrderService orderService;
private String beanName;
public String getBeanName() {
return beanName;
}
public OrderService getOrderService() {
return orderService;
}
@Override
public void setBeanName(String name) {
this.beanName=name;
}
}
if(instance instanceof BeanNameAware){
// 如果instance实现类BeanNameAware接口
((BeanNameAware) instance).setBeanName(beanName);
}
private Object createBean(String beanName,BeanDefinition beanDefinition) { try { // 根据beanDefinition对象创建bean对象 Class clazz = beanDefinition.getClazz(); Object instance = clazz.newInstance(); //获取类的所有成员变量 Field[] fields = clazz.getDeclaredFields(); for (Field field: fields) { // 如果该成员变量被@Autowired注解标识 if(field.isAnnotationPresent(Autowired.class)){ // 成员变量名称 String name = field.getName(); // 根据名称获取bean Object bean = getBean(name); field.setAccessible(true); // 将获取到的bean设置到类对象中 field.set(instance,bean); } } if(instance instanceof BeanNameAware){ // 如果instance实现类BeanNameAware接口 ((BeanNameAware) instance).setBeanName(beanName); } return instance; } catch (Exception e) { e.printStackTrace(); return null; } }
在上文中我们通过bean实现beanAware接口实现了给类的成员变量回调赋值,在Spring中提供了一个名为InitializingBean的接口,通过实现该接口,可以在bean初始化的时候根据用户的需要实现InitializingBean接口中并重写其中的方法
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
@Component("myService")
@Scope("prototype")
public class MyService implements BeanNameAware, InitializingBean {
@Autowired
private OrderService orderService;
private String beanName;
public String getBeanName() {
return beanName;
}
public OrderService getOrderService() {
return orderService;
}
@Override
public void setBeanName(String name) {
this.beanName=name;
}
// 重写 InitializingBean 抽象方法
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("调用了afterPropertiesSet方法");
}
}
// 初始化
if(instance instanceof InitializingBean){
//如果instance实现类InitializingBean接口,则调用其抽象方法
((InitializingBean) instance).afterPropertiesSet();
}
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
//BeanPostProcessor
private List<BeanPostProcessor> beanPostProcessorList=new ArrayList<>();
@Component("myService")
@Scope("prototype")
public class MyService implements BeanNameAware, InitializingBean,BeanPostProcessor {
...
// 重写 InitializingBean 抽象方法
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("调用了afterPropertiesSet方法");
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("初始化之前运行"+beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("初始化之后运行"+beanName);
return bean;
}
}
GitHub 上有什么好玩的项目?
SpringSecurity + JWT 实现单点登录
4. 100 道 Linux 常见面试题
本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。