操操操

第4章:崭露头角,基于Cglib实现含构造函数的类实例化策略

2021-05-30
8分钟阅读时长

作者:秋小官(小秋哥) 博客:https://zuisishu.com(opens new window) 原文:<https://zuisishu.com

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

技术成长,是对场景设计细节不断的雕刻!

你觉得自己的技术什么时候得到了快速的提高,是CRUD写的多了以后吗?想都不要想,绝对不可能!CRUD写的再多也只是能满足你作为一个搬砖工具人,敲击少逻辑流水代码的速度而已,而编程能力这一块,除了最开始的从不熟练到熟练以外,就很少再有其他提升了。

那你可能会想什么才是编程能力提升?其实更多的编程能力的提升是你对复杂场景的架构把控以及对每一个技术实现细节点的不断用具有规模体量的流量冲击验证时,是否能保证系统稳定运行从而决定你见识了多少、学到了多少、提升了多少!

最终当你在接一个产品需求时,开始思考程序数据结构的设计、核心功能的算法逻辑实现、整体服务的设计模式使用、系统架构的搭建方式、应用集群的部署结构,那么也就是的编程能力真正提升的时候!

二、目标

这一章节的目标主要是为了解决上一章节我们埋下的坑,那是什么坑呢?其实就是一个关于 Bean 对象在含有构造函数进行实例化的坑。

在上一章节我们扩充了 Bean 容器的功能,把实例化对象交给容器来统一处理,但在我们实例化对象的代码里并没有考虑对象类是否含构造函数,也就是说如果我们去实例化一个含有构造函数的对象那么就要抛异常了。

怎么验证?其实就是把 UserService 添加一个含入参信息的构造函数就可以,如下:

public class UserService {

private String name;

public UserService(String name) {
    this.name = name;
}  

// ...

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

报错如下:

java.lang.InstantiationException: cn.zuisishu.springframework.test.bean.UserService

at java.lang.Class.newInstance(Class.java:427) at cn.zuisishu.springframework.test.ApiTest.test_newInstance(ApiTest.java:51) …

    @秋小官(小秋哥): 代码已经复制到剪贴板

发生这一现象的主要原因就是因为 beanDefinition.getBeanClass().newInstance(); 实例化方式并没有考虑构造函数的入参,所以就这个坑就在这等着你了!那么我们的目标就很明显了,来把这个坑填平!

三、设计

填平这个坑的技术设计主要考虑两部分,一个是串流程从哪合理的把构造函数的入参信息传递到实例化操作里,另外一个是怎么去实例化含有构造函数的对象。

图 4-1

参考 Spring Bean 容器源码的实现方式,在 BeanFactory 中添加 Object getBean(String name, Object… args) 接口,这样就可以在获取 Bean 时把构造函数的入参信息传递进去了。 另外一个核心的内容是使用什么方式来创建含有构造函数的 Bean 对象呢?这里有两种方式可以选择,一个是基于 Java 本身自带的方法 DeclaredConstructor,另外一个是使用 Cglib 来动态创建 Bean 对象。Cglib 是基于字节码框架 ASM 实现,所以你也可以直接通过 ASM 操作指令码来创建对象

四、实现

1. 工程结构

small-spring-step-03 └── src ├── main │ └── java │ └── cn.zuisishu.springframework.beans │ ├── factory │ │ ├── config │ │ │ ├── BeanDefinition.java │ │ │ └── SingletonBeanRegistry.java │ │ ├── support │ │ │ ├── AbstractAutowireCapableBeanFactory.java │ │ │ ├── AbstractBeanFactory.java │ │ │ ├── BeanDefinitionRegistry.java │ │ │ ├── CglibSubclassingInstantiationStrategy.java │ │ │ ├── DefaultListableBeanFactory.java │ │ │ ├── DefaultSingletonBeanRegistry.java │ │ │ ├── InstantiationStrategy.java │ │ │ └── SimpleInstantiationStrategy.java │ │ └── BeanFactory.java │ └── BeansException.java └── test └── java └── cn.zuisishu.springframework.test ├── bean │ └── UserService.java └── ApiTest.java

    @秋小官(小秋哥): 代码已经复制到剪贴板

工程源码:公众号「秋小官」,回复:Spring 专栏,获取完整源码

Spring Bean 容器类关系,如图 4-2

图 4-2

本章节“填坑”主要是在现有工程中添加 InstantiationStrategy 实例化策略接口,以及补充相应的 getBean 入参信息,让外部调用时可以传递构造函数的入参并顺利实例化。

2. 新增 getBean 接口

cn.zuisishu.springframework.beans.factory.BeanFactory

public interface BeanFactory {

Object getBean(String name) throws BeansException;

Object getBean(String name, Object... args) throws BeansException;

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

BeanFactory 中我们重载了一个含有入参信息 args 的 getBean 方法,这样就可以方便的传递入参给构造函数实例化了。

3. 定义实例化策略接口

cn.zuisishu.springframework.beans.factory.support.InstantiationStrategy

public interface InstantiationStrategy {

Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException;

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

在实例化接口 instantiate 方法中添加必要的入参信息,包括:beanDefinition、 beanName、ctor、args 其中 Constructor 你可能会有一点陌生,它是 java.lang.reflect 包下的 Constructor 类,里面包含了一些必要的类信息,有这个参数的目的就是为了拿到符合入参信息相对应的构造函数。 而 args 就是一个具体的入参信息了,最终实例化时候会用到。

4. JDK 实例化

cn.zuisishu.springframework.beans.factory.support.SimpleInstantiationStrategy

public class SimpleInstantiationStrategy implements InstantiationStrategy {

@Override
public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
    Class clazz = beanDefinition.getBeanClass();
    try {
        if (null != ctor) {
            return clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);
        } else {
            return clazz.getDeclaredConstructor().newInstance();
        }
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
        throw new BeansException("Failed to instantiate [" + clazz.getName() + "]", e);
    }
}

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

首先通过 beanDefinition 获取 Class 信息,这个 Class 信息是在 Bean 定义的时候传递进去的。 接下来判断 ctor 是否为空,如果为空则是无构造函数实例化,否则就是需要有构造函数的实例化。 这里我们重点关注有构造函数的实例化,实例化方式为 clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);,把入参信息传递给 newInstance 进行实例化。

5. Cglib 实例化

cn.zuisishu.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy

public class CglibSubclassingInstantiationStrategy implements InstantiationStrategy {

@Override
public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(beanDefinition.getBeanClass());
    enhancer.setCallback(new NoOp() {
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    });
    if (null == ctor) return enhancer.create();
    return enhancer.create(ctor.getParameterTypes(), args);
}

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

其实 Cglib 创建有构造函数的 Bean 也非常方便,在这里我们更加简化的处理了,如果你阅读 Spring 源码还会看到 CallbackFilter 等实现,不过我们目前的方式并不会影响创建。

6. 创建策略调用

cn.zuisishu.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory {

private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

@Override
protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
    Object bean = null;
    try {
        bean = createBeanInstance(beanDefinition, beanName, args);
    } catch (Exception e) {
        throw new BeansException("Instantiation of bean failed", e);
    }

    addSingleton(beanName, bean);
    return bean;
}

protected Object createBeanInstance(BeanDefinition beanDefinition, String beanName, Object[] args) {
    Constructor constructorToUse = null;
    Class<?> beanClass = beanDefinition.getBeanClass();
    Constructor<?>[] declaredConstructors = beanClass.getDeclaredConstructors();
    for (Constructor ctor : declaredConstructors) {
        if (null != args && ctor.getParameterTypes().length == args.length) {
            constructorToUse = ctor;
            break;
        }
    }
    return getInstantiationStrategy().instantiate(beanDefinition, beanName, constructorToUse, args);
}

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

首先在 AbstractAutowireCapableBeanFactory 抽象类中定义了一个创建对象的实例化策略属性类 InstantiationStrategy instantiationStrategy,这里我们选择了 Cglib 的实现类。 接下来抽取 createBeanInstance 方法,在这个方法中需要注意 Constructor 代表了你有多少个构造函数,通过 beanClass.getDeclaredConstructors() 方式可以获取到你所有的构造函数,是一个集合。 接下来就需要循环比对出构造函数集合与入参信息 args 的匹配情况,这里我们对比的方式比较简单,只是一个数量对比,而实际 Spring 源码中还需要比对入参类型,否则相同数量不同入参类型的情况,就会抛异常了。

五、测试

1. 事先准备

cn.zuisishu.springframework.test.bean.UserService

public class UserService {

private String name;

public UserService(String name) {
    this.name = name;
}

public void queryUserInfo() {
    System.out.println("查询用户信息:" + name);
}

@Override
public String toString() {
    final StringBuilder sb = new StringBuilder("");
    sb.append("").append(name);
    return sb.toString();
}

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

这里唯一多在 UserService 中添加的就是一个有 name 入参的构造函数,方便我们验证这样的对象是否能被实例化。

2. 测试用例

cn.zuisishu.springframework.test.ApiTest

@Test public void test_BeanFactory() { // 1.初始化 BeanFactory DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

// 2. 注入bean
BeanDefinition beanDefinition = new BeanDefinition(UserService.class);
beanFactory.registerBeanDefinition("userService", beanDefinition);

// 3.获取bean
UserService userService = (UserService) beanFactory.getBean("userService", "秋小官(小秋哥)");
userService.queryUserInfo();

}

    @秋小官(小秋哥): 代码已经复制到剪贴板

在此次的单元测试中,依然包括包括三个核心步骤;初始化 BeanFactory 工厂、注册 Bean、获取 Bean,此外与上一章节不同的是,在获取 Bean 对象时候,传递了一个参数名称为“秋小官(小秋哥)”的入参信息,这个信息的传递将会帮我们创建出含有 String 类型构造函数的 UserService 类,而不会再出现初始化报错的问题。

3. 测试结果

查询用户信息:秋小官(小秋哥)

Process finished with exit code 0

    @秋小官(小秋哥): 代码已经复制到剪贴板

从测试结果来看,最大的变化就是可以满足带有构造函数的对象,可以被实例化了。 你可以尝试分别使用两种不同的实例化策略,来进行实例化。SimpleInstantiationStrategy、CglibSubclassingInstantiationStrategy

4. 操作案例

这里我们再把几种不同方式的实例化操作,放到单元测试中,方便大家比对学习。

4.1 无构造函数

@Test public void test_newInstance() throws IllegalAccessException, InstantiationException { UserService userService = UserService.class.newInstance(); System.out.println(userService); }

    @秋小官(小秋哥): 代码已经复制到剪贴板

这种方式的实例化也是我们在上一章节实现 Spring Bean 容器时直接使用的方式

4.2 验证有构造函数实例化

@Test public void test_constructor() throws Exception { Class userServiceClass = UserService.class; Constructor declaredConstructor = userServiceClass.getDeclaredConstructor(String.class); UserService userService = declaredConstructor.newInstance(“秋小官(小秋哥)”); System.out.println(userService); }

    @秋小官(小秋哥): 代码已经复制到剪贴板

从最简单的操作来看,如果有构造函数的类需要实例化时,则需要使用 getDeclaredConstructor 获取构造函数,之后在通过传递参数进行实例化。

4.3 获取构造函数信息

@Test public void test_parameterTypes() throws Exception { Class beanClass = UserService.class; Constructor[] declaredConstructors = beanClass.getDeclaredConstructors(); Constructor constructor = declaredConstructors[0]; Constructor declaredConstructor = beanClass.getDeclaredConstructor(constructor.getParameterTypes()); UserService userService = declaredConstructor.newInstance(“秋小官(小秋哥)”); System.out.println(userService);

    @秋小官(小秋哥): 代码已经复制到剪贴板

这个案例中其实最核心的点在于获取一个类中所有的构造函数,其实也就是这个方法的使用 beanClass.getDeclaredConstructors()

4.4 Cglib 实例化

@Test public void test_cglib() { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new NoOp() { @Override public int hashCode() { return super.hashCode(); } }); Object obj = enhancer.create(new Class[]{String.class}, new Object[]{“秋小官(小秋哥)”}); System.out.println(obj); }

    @秋小官(小秋哥): 代码已经复制到剪贴板

此案例演示使用非常简单,但关于 Cglib 在 Spring 容器中的使用非常多,也可以深入的学习一下 Cglib 的扩展知识。

六、总结

本章节的主要以完善实例化操作,增加 InstantiationStrategy 实例化策略接口,并新增了两个实例化类。这部分类的名称与实现方式基本是 Spring 框架的一个缩小版,大家在学习过程中也可以从 Spring 源码找到对应的代码。 从我们不断的完善增加需求可以看到的,当你的代码结构设计的较为合理的时候,就可以非常容易且方便的进行扩展不同属性的类职责,而不会因为需求的增加导致类结构混乱。所以在我们自己业务需求实现的过程中,也要尽可能的去考虑一个良好的扩展性以及拆分好类的职责。 动手是学习起来最快的方式,不要让眼睛是感觉看会了,但上手操作就废了。也希望有需要的读者可以亲手操作一下,把你的想法也融入到可落地实现的代码里,看看想的和做的是否一致。