java---手撕 Spring

1,374 阅读14分钟

手撕 Spring

前言

 本博文适用于对于 Spring 执行过程有过了解的读者,文中没有过多的去解释对象的职责,更多的是讲怎样去实现框架的功能,如果是小白的话看起来可能会比较晦涩,不懂的地方可以在下方留言进行解答。

 项目完整地址:gitee.com/BeOrDie/BOD…

1、Spring IOC

IOC(Inversion of Control) 叫做控制反转,也就是说不需要程序员去自动的管理对象的创建的,而是交由容器去管理,而容器要创建对象必然需要知道哪些对象是需要进行创建的,需要配置什么样的参数等等。要做到上面的要求,我们就得有一个缓存去保存这些数据,而这些数据可以通过配置或者是注解的方式给出,让程序自动去扫描解析保存相关关系即可。

 我们首先来分析一下源码中对于这部分数据的管理是怎样进行的,通过时序图可以发现最终的关系都被保存了一个叫做 beanDefinitionMap 中了,里面的数据对象是一个 BeanDefinition 类型的,也就是说我们需要将托管的对象全部封装成 BeanDefinition 对象,里面保存着数据信息,然后全部填充在一个 Map 集合中使用。

IOC.jpg

 1.1、配置保存

 首先需要定义这样的一个 BeanDefinition 类来保存解析的托管数据,源码中可以定义的参数很多,我们就简单一点,只定义部分属性就可以了(相信延迟加载、单例创建属性的关系读者已经明白了,这里就不解释了)。

public class BODBeanDefinition {
    /**
     * 是否延迟加载
     * @return
     */
    public boolean isLazyInit() { return false; }

    /**
     * 单例
     * @return
     */
    public boolean isSingleton() { return true; }

    /**
     * 对象映射用的名字
     */
    private String factoryBeanName;

    /**
     * 全类名
     */
    private String beanClassName;
}

 1.2、配置解析

 通过时序图,发现有一个 DefaultBeanDefinitionDocumentReader 的类执行了 parse 的过程,我们也创建一个 Reader 的类来完成配置的解析。而一般的配置解析都会分为以下几步,我们也按照流程来执行:

  1. 加载配置文件(可以是XML、properties、yaml等,为了方便我们就直接用 Properties 来完成加载读取)
  2. 解析配置文件(这里对应的就是对配置文件指定包路径进行扫描,将符合托管规则的类进行加载)
  3. 保存配置信息(将扫描到的类信息进行保存,交给之后的 BeanDefinition 进行封装)
public class BODBeanDefinitionReader {
    /**
     * 配置文件信息
     */
    private Properties contextConfig = new Properties();

    /**
     * 需要进行注册的 bean
     */
    private List<String> registerBeanClasses = new ArrayList<>();

    /**
     * 加载扫描配置文件
     * @param locations
     */
    public BODBeanDefinitionReader(String ... locations) {
        //  1.加载配置文件信息流
        String parameter = locations[0];
        try {
            InputStream stream = BODBeanDefinitionReader.class.getClassLoader().getResourceAsStream(parameter);
            this.contextConfig.load(stream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //  2.扫描相关类
        scanPackage(this.contextConfig.getProperty("base"));
    }

    /**
     * 创建映射 BeanDefinition 关系
     * @return
     */
    public List<BODBeanDefinition> loadBeanFefinitions() {
        List<BODBeanDefinition> result = new ArrayList<>();
        try {
            for (String className : this.registerBeanClasses) {
                Class<?> beanClass = Class.forName(className);
                //  是接口就跳过
                if (beanClass.isInterface()) { continue; }

                //  1.默认首字母小写为映射名
                result.add(createBeanDefinition(StringUtil.lowerCaseFisrt(beanClass.getSimpleName()), beanClass.getName()));

                //  2.接口用实现类代替
                for (Class<?> inter : beanClass.getInterfaces()) {
                    result.add(createBeanDefinition(inter.getName(), beanClass.getName()));
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 封装 beanDefinition 对象
     * @param factoryName
     * @param className
     * @return
     */
    private BODBeanDefinition createBeanDefinition(String factoryName, String className) {
        BODBeanDefinition definition = new BODBeanDefinition();
        definition.setFactoryBeanName(factoryName);
        definition.setBeanClassName(className);
        return definition;
    }

    /**
     * 扫描包路径下的所有类保存
     * @param packageName
     */
    private void scanPackage(String packageName) {
        URL url = BODBeanDefinitionReader.class.getClassLoader().getResource("/" + packageName.replaceAll("\\.", "/"));
        File file = new File(url.getFile());
        for (File path : file.listFiles()) {
            // 包继续遍历
            if (path.isDirectory()) {
                scanPackage(packageName + "." + path.getName());
            } else {
                // 非字节文件不进行处理
                if (!path.getName().endsWith("class")) {
                    continue;
                }
                String className = packageName + "." + path.getName().substring(0, path.getName().lastIndexOf(".class"));
                this.registerBeanClasses.add(className);
            }
        }
    }

    /**
     * 获取配置值
     * @param parameter
     * @return
     */
    public String getConfig(String parameter) {
        return this.contextConfig.getProperty(parameter);
    }
}

 1.3、IOC 注册

 数据解析完毕之后,由一个 DefaultListableBeanFactory 的工厂来完成 IOC 的注册的,也就是在工厂中会来到前面解析的配置数据,然后封装成对象注册到 Map 中供后续步骤的使用。这里实现起来就比较简单了,只需要将解析之后的数据拿出来,并且保存一个 IOC 容器的缓存,把元数据封装成定义的配置对象塞入缓存中即可。

public class BODDefaultListableBeanFactory implements BODBeanFactory {
    /**
     * 存储 class、name 到 beanDefinition 的映射
     */
    public Map<String, BODBeanDefinition> bodBeanDefinitionMap = new HashMap<>();

    @Override
    public Object getBean(Class beabClass) {
        return null;
    }

    @Override
    public Object getBean(String beanName) {
        return null;
    }

    /**
     * 转换映射方式
     * @param beanDefinitions
     */
    public void doRegistBeanDefinition(List<BODBeanDefinition> beanDefinitions) {
        for (BODBeanDefinition beanDefinition : beanDefinitions) {
            if (this.bodBeanDefinitionMap.containsKey(beanDefinition.getFactoryBeanName())) {
                throw new Error("this beanName " + beanDefinition.getFactoryBeanName() + "is exist.");
            }
            this.bodBeanDefinitionMap.put(beanDefinition.getFactoryBeanName(), beanDefinition);
        }
    }
}

 到这里整个 IOC 的部分就已经完成了,因为这部分只需要拿到元数据信息保存在容器中就可以了,就是针对于配置文件的解析、注解扫描匹配等,还是比较简单的。按道理说这里应该 IOC 容器中应该保存着的是对应的实例,但是我们后面注入的时候才会进行对象的创建,如果首先对象创建好的话就和 DI 部分纠缠(虽然这两个概念本来就是一起的,为了能够完成手撕的逻辑,还是进行分开说明)。

2、Spring DI

 2.1、对象实例化

DI 是 Spring 能够完成对象托管的一个重要步骤,他能根据前面 IOC 容器中的元数据自动进行对象的创建和自动属性的注入,极大地方便了我们的开发管理。具体的原理就是把 IOC 容器中的配置取出来进行对象的创建,然后检查内部的属性是否具备自动注入的条件,有的话通过反射的方式进行注入。

/**
 * IOC 对象实例化
*/
private void doLoadInstance() {
    for (Map.Entry<String, BODBeanDefinition> entry : this.register.bodBeanDefinitionMap.entrySet()) {
        String beanName = entry.getKey();
        if (!entry.getValue().isLazyInit()) {
            getBean(beanName);
        }
    }
}

@Override
public Object getBean(Class beabClass) {
    return getBean(beabClass.getName());
}

@Override
public Object getBean(String beanName) {
    //  1.获取 beanDefinition 配置信息
    BODBeanDefinition definition = this.register.bodBeanDefinitionMap.get(beanName);
    Object singleton = getSingleton(beanName, definition);
    if (singleton != null) { return singleton; }
    if (!this.singletonCurrentlyInCreation.contains(beanName)) {
        this.singletonCurrentlyInCreation.add(beanName);
    }
    //  2.实例化对象
    Object instance = instantiateBean(beanName, definition);
    //  3.封装 wrapper 对象
    BODBeanWrapper wrapper = new BODBeanWrapper(instance);
    //  4.依赖注入
    populateBean(beanName, definition, wrapper);
    //  5.保存 IOC 容器
    this.factoryBeanInstanceCache.put(beanName, wrapper);

    return wrapper.getWrappedInstance();
}

 2.2、依赖注入

 依赖注入期间存在的问题就是循环依赖,也就是在我们对一号对象进行依赖注入时发现需要的二号对象没有创建,因此我们会先去创建二号对象,但是在创建二号对象的同时也会对其进行属性注入,如果这个时候发现二号对象属性中需要一号对象的引用,虽然我们的一号对象已经进行创建,但是他的整个注入过程没有结束,也就是并没有在缓存中存在,这个时候二号注入的时候就拿不到引用,这样就陷入了一个循环。

 而解决这个问题的方法就是使用多级缓存,一旦对象创建之后就进行原生对象的保存,这个时候对象里面的所有属性都是干净的。这个时候二号对象的属性就可以从这个缓存中取出数据进行注入,注入完成之后再将对象给缓存起来,这个缓存就是需要交给容器去管理的实际使用对象。

/**
     * 先期获取对象
     * @param beanName
     * @param definition
     * @return
     */
private Object getSingleton(String beanName, BODBeanDefinition definition) {
    //  1.一级缓存中拿
    Object bean = this.singletonObjects.get(beanName);
    //  2.一级缓存没有, 有创建标记则表示循环依赖
    if (bean == null && this.singletonCurrentlyInCreation.contains(beanName)) {
        bean = this.earlySingletonObjects.get(beanName);
        //  3.从三级缓存中拿, 也就是进行对象的创建
        if (bean == null) {
            bean = instantiateBean(beanName, definition);
            this.earlySingletonObjects.put(beanName, bean);
        }
    }
    return bean;
}

/**
     * DI 注入
     * @param beanName
     * @param definition
     * @param wrapper
     */
private void populateBean(String beanName, BODBeanDefinition definition, BODBeanWrapper wrapper) {
    Object instance = wrapper.getWrappedInstance();
    Class<?> clazz = wrapper.getWrappedClass();

    if (!(clazz.isAnnotationPresent(BODController.class) || clazz.isAnnotationPresent(BODService.class))) {
        return;
    }
    for (Field field : clazz.getDeclaredFields()) {
        String name = null;
        if (field.isAnnotationPresent(BODAutowried.class)) { // 按照类型注入
            name = field.getName();
        } else if (field.isAnnotationPresent(BODResource.class)) { // 按照名称进行注入
            name = field.getAnnotation(BODResource.class).name().trim();
            if ("".equals(name)) name = field.getName();
        } else {
            continue;
        }
        //  字段进行赋值
        field.setAccessible(true);
        try {
            field.set(instance, getBean(name));
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

3、Spring AOP

 相信大家都知道 Spring AOP 的底层是由 JDK动态代理Cglib动态代理 实现的(如果关于这两个代理的底层实现不太了解可以去看一下之前的关于设计模式的文章),但是如果没有看过源码的实现的话,估计我们的实现就会变成这样。

public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
       	before();
    	arround();
    	Object result = null; 
    	try {
            result = method.invoke(objects);
        } catch (Exceprion e) {
            exception();
        }
    	arround();
    	return result; 
    }

 这样的实现其实也能达到代码织入增强的目的,但是这样的硬编码首先就把织入的功能给限定了,不具备拓展性,就显得很笨重。

 通过追踪分析 Spring AOP 这一块的源码,我们可以知道官方针对功能的增强使用的责任链的方式,也就是织入按链执行,而且增强的功能也按照不同的位置进行类的划分,也就是外部隐藏了执行位置,只需要执行具体的方法就可以了,内部会自动按照功能的对应位置进行执行。如果有很多次的方法增强,也只需要往执行器链中添加对象即可。

AOP.jpg

 3.1、切面规则

 既然要进行代码增强,我们首先就得先知道需要进行什么样的增强,在哪里进行增强。那我们就可以定义一个切面规则来进行切面配置的存储,方便后续对于切面的判定。BODAopConfig 这个类的主要作用就是加载系统外部定义的切面的配置信息,解析具体的切面表达式,将符合切面规则的对象都进行指定切面的增强。

public class BODAopConfig {
    //	切面表达式 public .* com.beordie.test.service..*Service..*(.*)
    private String pointCut;
    //	切面对象 com.beordie.aop.Log
    private String aspectClass;
    //	前置增强
    private String aspectBefore;
    //	后置增强
    private String aspectAfter;
    //	异常捕获
    private String aspectAfterThrow;
    //	需要捕获的具体异常
    private String aspectAfterThrowingName;
}

 在 Spring 中进行一些增强时可以对数据进行更一步的修改执行,也就是我们需要这样的一个对象来保存整个过程中方法的放回对象,方便我们在切面中进行调出修改。BODJoinPoint 接口定义了过程中对象应具有的规则,具体的数据存储交给实现类去处理。

public interface BODJoinPoint {
    /**
     * 实例对象
     * @return
     */
    Object getThis();

    /**
     * 获取参数列表
     * @return
     */
    Object[] getArgument();

    /**
     * 代理方法
     * @return
     */
    Method getMethod();

    /**
     * 设置属性
     * @param key
     * @param value
     */
    void setUserAttribute(String key, Object value);

    /**
     * 获取属性
     * @param key
     * @return
     */
    Object getUserAttribute(String key);
}

 对于哪些目标对象需要增强,我们在开始时并不知道的,只有在进行获取目标对象的时候才能进行判断,而且需要在代理中进行切面对象的织入,也就是我们需要识别目标对象和保存方法对应的切面对象供后续的织入使用。一个方法可能会被多个切面对象的增强,也就是需要保持一个缓存(method -- 切面对象集合)。BODAdviseSupport 类的主要职责就是解析切面和保存映射关系,需要在创建对象的时候执行,好判断当前的对象是否满足代理的要求,从而进行代理包装,也就是经过这步处理的对象将不会是原生对象。

public class BODAdviseSupport {
    /**
     * 保存方法和增强之间的关系
     */
    private Map<Method, List<Object>> methodCache;
    /**
     * 类型正则匹配
     */
    private Pattern pointCutClassPattern;
    /**
     * 目标对象
     */
    private Object targetInstance;
    /**
     * 目标类
     */
    private Class targetClass;
    /**
     * 切面规则
     */
    private BODAopConfig config;

    public BODAdviseSupport(BODAopConfig config) {
        this.config = config;
    }

    public Object getTargetInstance() {
        return targetInstance;
    }

    public void setTargetInstance(Object targetInstance) {
        this.targetInstance = targetInstance;
    }

    /*
    	针对于切面表达式的解析没有进行过多的设计
    */
    private void parse() {
        String pointCutRegex = this.config.getPointCut()
                .replaceAll("\\.", "\\\\.")
                .replaceAll("\\\\.\\*", ".*")
                .replaceAll("\\(", "\\\\(")
                .replaceAll("\\)", "\\\\)");
        String pointCutForClassRegex = pointCutRegex.substring(0, pointCutRegex.lastIndexOf("\\(") - 4);
        this.pointCutClassPattern = Pattern.compile("class " +
                pointCutForClassRegex.substring(pointCutForClassRegex.lastIndexOf(" ") + 1));
        this.methodCache = new HashMap<>();
        Pattern pointCutPattern = Pattern.compile(pointCutRegex);
        try {
            Map<String, Method> aspectMethods = new HashMap<>();
            Class<?> clazz = Class.forName(this.config.getAspectClass());
            //  迭代切面对象进行方法缓存
            for (Method method : clazz.getMethods()) {
                aspectMethods.put(method.getName(), method);
            }
            //  匹配目标对象的方法规则
            for (Method method : this.targetClass.getMethods()) {
                String methodString = method.toString();
                //  去除异常抛出的影响
                if (methodString.contains("throws")) {
                    methodString = methodString.substring(0, methodString.lastIndexOf("throws")).trim();
                }
                Matcher matcher = pointCutPattern.matcher(methodString);
                if (matcher.matches()) {
                    //  注意是切面对象目标
                    List<Object> advices = new LinkedList<>();
                    if (!(this.config.getAspectBefore() == null || "".equals(this.config.getAspectBefore().trim()))) {
                        advices.add(
                                new BODBeforeAdviceInterceptor(
                                        clazz.newInstance(), aspectMethods.get(this.config.getAspectBefore())));
                    }
                    if (!(this.config.getAspectAfter() == null || "".equals(this.config.getAspectAfter().trim()))) {
                        advices.add(
                                new BODAfterReturningAdviceInterceptor(
                                        clazz.newInstance(), aspectMethods.get(this.config.getAspectAfter())));
                    }
                    if (!(this.config.getAspectAfterThrow() == null || "".equals(this.config.getAspectAfterThrow().trim()))) {
                        BODThrowingAdviceInterceptor advice =
                                new BODThrowingAdviceInterceptor(clazz.newInstance(), aspectMethods.get(this.config.getAspectAfterThrow()));
                        advice.setThrowName(this.config.getAspectAfterThrowingName());
                        advices.add(advice);
                    }
                    this.methodCache.put(method, advices);
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    public Class getTargetClass() {
        return targetClass;
    }

    public void setTargetClass(Class targetClass) {
        this.targetClass = targetClass;
        parse();
    }

    public boolean pointCutMatch() {
        return this.pointCutClassPattern.matcher(this.targetClass.toString()).matches();
    }

    public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, Class targetClass) throws NoSuchMethodException {
        List<Object> cache = this.methodCache.get(method);
        if (cache == null) {
            Method classMethod = targetClass.getMethod(method.getName(), method.getParameterTypes());
            cache = this.methodCache.get(classMethod);
            this.methodCache.put(classMethod, cache);
        }
        return cache;
    }
}

 3.2、切面对象

 前面说过方法的增强是使用责任链的方式来执行的,也就是说我们需要一个统一的执行对象来填充责任链。而每一个切面对象都需要知道具体的目标实例和增强方法,我们就可以把这部分提取出来,供所有的切面对象使用。

public class BODAbstractAspectJAdvice implements BODAdvice{
    /**
     * 织入对象
     */
    private Object aspect;
    /**
     * 增强方法
     */
    private Method adviceMethod;
    /**
     * 异常名称
     */
    private String throwName;

    public BODAbstractAspectJAdvice(Object aspect, Method adviceMethod) {
        this.aspect = aspect;
        this.adviceMethod = adviceMethod;
    }

    public void setThrowName(String throwName) {
        this.throwName = throwName;
    }

    /**
     * 负责对参数的处理执行
     */
    protected Object invokeAdviceMethod(BODJoinPoint joinPoint, Object returnValue, Throwable ex)
            throws Throwable {
        Class<?> [] paramTypes = this.adviceMethod.getParameterTypes();
        if(null == paramTypes || paramTypes.length == 0){
            return this.adviceMethod.invoke(aspect);
        }else {
            Object[] args = new Object[paramTypes.length];
            for (int i = 0; i < paramTypes.length; i++) {
                if (paramTypes[i] == BODJoinPoint.class) {
                    args[i] = joinPoint;
                } else if (paramTypes[i] == Throwable.class) {
                    args[i] = ex;
                } else if (paramTypes[i] == Object.class) {
                    args[i] = returnValue;
                }
            }
            return this.adviceMethod.invoke(aspect, args);
        }
    }
}

 上面的切面规则中定义了三个不同的切面对象,那我们就需要将这三个拦截的具体的执行进行实现,整体的实现流程是一样的,指示针对于不同位置的增强需要调换一下具体方法的执行顺序。

  • 前置切面

    public class BODBeforeAdviceInterceptor extends BODAbstractAspectJAdvice implements BODMethodInterceptor{
        /**
         * 参数
         */
        private BODJoinPoint joinPoint;
    
        public BODBeforeAdviceInterceptor(Object aspect, Method adviceMethod) {
            super(aspect, adviceMethod);
        }
    
        @Override
        public Object invoke(BODMethodInvocation invocation) throws Throwable {
            this.joinPoint = invocation;
            //	因为是前置的原因,增强方法需要在具体的方法执行之前进行执行
            this.before(invocation.getMethod(), invocation.getArgument(), invocation.getThis());
            return invocation.procced();
        }
    
        public void before(Method method, Object[] arguments, Object aThis) throws Throwable{
            invokeAdviceMethod(this.joinPoint,null,null);
        }
    }
    
  • 后置切面

    public class BODAfterReturningAdviceInterceptor extends BODAbstractAspectJAdvice implements BODMethodInterceptor{
        /**
         * 参数对象
         */
        private BODJoinPoint joinPoint;
    
        public BODAfterReturningAdviceInterceptor(Object newInstance, Method method) {
            super(newInstance, method);
        }
    
        @Override
        public Object invoke(BODMethodInvocation invocation) throws Throwable {
            this.joinPoint = invocation;
            Object result = invocation.procced();
            //	因为是后置切面,因此需要在具体的方法执行完毕之后才能进行增强,这时候可以拿到具体的返回值进行进一步的修改
            this.afterReturning(result, invocation.getMethod(), invocation.getArgument(), invocation.getThis());
            return null;
        }
    
        private void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable{
            this.invokeAdviceMethod(this.joinPoint, returnValue,null);
        }
    }
    
  • 异常切面

    public class BODThrowingAdviceInterceptor extends BODAbstractAspectJAdvice implements BODMethodInterceptor{
        /**
         * 异常类型
         */
        private String throwName;
        public BODThrowingAdviceInterceptor(Object newInstance, Method method) {
            super(newInstance, method);
        }
    
        @Override
        public Object invoke(BODMethodInvocation invocation) throws Throwable {
            try {
                return invocation.procced();
            }catch (Throwable ex) {
                //	异常切面只需要在目标发生具体指定的异常时才会进行执行
                invokeAdviceMethod(invocation, null, ex);
                throw ex;
            }
        }
    
        public void setThrowName(String aspectAfterThrowingName) {
            this.throwName = aspectAfterThrowingName;
        }
    }
    

 3.3、责任链

 这里的责任链执行采用外驱的方式,也就是由先确定责任链的长度,然后按序进行执行,直到责任链的末尾结束。因为前面的切面对象列表已经获取好了,因此这里只需要将其取出来使用即可。同时这个需要实现 BODJoinPoint 定义存储数据的容器,供整个过程中临时数据的存储。

public class BODMethodInvocation implements BODJoinPoint {
    //	代理类
    protected final Object proxy;
    //	目标类
    protected final Object target;
    //	目标类型
    private final Class<?> targetClass;
    //	增强方法
    protected final Method method;
    //	方法参数
    protected Object[] arguments;
    //	切面对象列表
    protected final List<?> interceptorsAndDynamicMethodMatchers;
    //	执行的索引
    private Integer currentInterceptorIndex = -1;
    //	存放临时数据,也就是 BODJoinPoint 存储的数据
    private Map<String, Object> userAttribute;

    public BODMethodInvocation(Object o, Object targetInstance, Method method, Object[] objects, Class targetClass, List<Object> chain) {
        this.proxy = o;
        this.target = targetInstance;
        this.targetClass = targetClass;
        this.method = method;
        this.arguments = objects;
        this.interceptorsAndDynamicMethodMatchers = chain;
        this.userAttribute = new HashMap<>();
    }

    public Object procced() throws Throwable{
        //  最后一个执行链调用自己
        if (this.interceptorsAndDynamicMethodMatchers == null
                || this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
            return this.method.invoke(this.target, this.arguments);
        }
        //  获取下一个执行
        Object o = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
        if (o instanceof BODMethodInterceptor) {
            BODMethodInterceptor interceptor = (BODMethodInterceptor) o;
            return interceptor.invoke(this);
        } else {
            return procced();
        }
    }

    @Override
    public Object getThis() {
        return this.target;
    }

    @Override
    public Object[] getArgument() {
        return this.arguments;
    }

    @Override
    public Method getMethod() {
        return this.method;
    }

    @Override
    public void setUserAttribute(String key, Object value) {
        this.userAttribute.put(key, value);
    }

    @Override
    public Object getUserAttribute(String key) {
        return this.userAttribute.get(key);
    }
}

 3.4、代理对象获取

 从时序图中我们可以知道,具体的代理对象是由 getProxy 这样的一个方法进行返回的,而且是由一个抽象工厂返回,我们这里就可以定义这样的一个接口来规定代理的生成,然后具体使用使用 JDK 的还是 Cglib 的就可以使用策略模式来进行判断(策略模式和我们经常用的判断语句一个样样的),在工厂中根据目标对象有没有实现接口来调用具体的代理生成对象。

  • 代理工厂

    public class BODDefaultAopProxyFactory {
        private BODAdviseSupport config;
    
        public BODAopProxy createAopProxy(BODAdviseSupport config) {
            if (config.getTargetClass().getInterfaces().length > 0) {
                return new BODJdkDynamicAopProxy(config);
            }
            return new BODCglibAopProxy(config);
        }
    }
    
  • 具体代理生成

    public class BODJdkDynamicAopProxy implements BODAopProxy, InvocationHandler {
        private BODAdviseSupport advices;
    
        public BODJdkDynamicAopProxy(BODAdviseSupport advices) {
            this.advices = advices;
        }
    
        @Override
        public Object getProxy() {
            BODAdviseSupport con = this.advices;
            return Proxy.newProxyInstance(
                    this.advices.getTargetClass().getClassLoader(), this.advices.getTargetClass().getInterfaces(), this);
        }
    
        @Override
        public Object getProxy(ClassLoader classLoader) {
            return null;
        }
    
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            //	获取切面对象集合
            List<Object> chain = this.advices.getInterceptorsAndDynamicInterceptionAdvice(method, this.advices.getTargetClass());
            //	执行责任链
            BODMethodInvocation invocation = new BODMethodInvocation(o, this.advices.getTargetInstance(), method, objects, this.advices.getTargetClass(), chain);
            return invocation.procced();
        }
    }
    

 到这里整个 AOP 的基本实现就已经解决了,让我们来看一看整个实现过程中的类之间的关系。

BODAbstractAspectJAdvice.png

4、Spring MVC

 学过 java web 的都知道使用 Servlet 的艰难,要么整个系统使用一个对外的 Servlet,要么就得创建很多个 Servlet 来完成不同路径的映射,而且都得继承来覆盖方法。参数的获取更是令人头疼,得从 Request 中一个一个的取数据,参数名称都写死了,不具备灵活性。因此为了简化开发成本,Spring MVC 应时出现,使得接口开发变得十分的简单。

 Spring MVC 提供了九大组件使用,我们这里就简单实现其中三个即可,组件的初始化应该是在服务器初始化时进行,也就是在 init 中调用具体的初始化步骤。

MVC.jpg

 由时序图我们可以发现,整个处理核心就是 DispatchServlet 类,这个类一定是继承了 Servlet 的才能完成对于请求的响应处理。具体的执行流程这里就不再赘述,相信大家都已经明白处理环节的,下面就直接开始手撕。

 4.1、地址映射

 传统开发中,一个 URL 请求进来之后会交由匹配地址的 Servlet 去处理,我们也需要维护这样的一个映射关系去保存 URL 到处理方法的过程,方便请求的处理。

public class BODHandlerMapping {
    private Object controller;
    protected Method method;
    protected Pattern url;

    public BODHandlerMapping(Method method, Object controller, Pattern url) {
        this.controller = controller;
        this.method = method;
        this.url = url;
    }
}

 具体的步骤只需要扫描类路径,将添加具体注解的类提出来,获取注解参数拼接成 URL 地址然后和具体的方法进行绑定,封装成上面的 Mapping 缓存即可。

private void initHandlerMappings(BODApplicationContext context) {
    if (this.applicationContext.getBeanDefinitionCount() == 0) { return ; }
    for (String beanName : this.applicationContext.getBeanDefinitionNames()) {
        Object bean = this.applicationContext.getBean(beanName);
        Class<?> aClass = bean.getClass();
        if (!aClass.isAnnotationPresent(BODController.class)) { continue; }
        String baseUrl = null;
        if (aClass.isAnnotationPresent(BODRequestMapping.class)) {
            baseUrl = aClass.getAnnotation(BODRequestMapping.class).value();
        }
        for (Method method : aClass.getMethods()) {
            if (!method.isAnnotationPresent(BODRequestMapping.class)) { continue; }
            BODRequestMapping annotation = method.getAnnotation(BODRequestMapping.class);
            String url = ("/" + baseUrl + "/" + annotation.value())
                .replaceAll("//*", ".*")
                .replaceAll("/+", "/");
            Pattern pattern = Pattern.compile(url);
            this.handlerMappings.add(new BODHandlerMapping(method, bean, pattern));
        }
    }
}

 这里有一个拼接 URL 的小技巧,因为我们不能约束开发者使用什么样的格式,但是实际上的地址都是由 / 分割的,因此我们只需要将类注解的参数和方法注解的参数添加 / 作为分隔符,然后将多余的分割用正则去掉即可。

 4.2、参数适配

 从前端进来的请求参数会放置在 Request 中,但是我们控制层方法的参数往往都是分开的,也就是需要我们提前将参数取出来和具体的形参达成适配。我们取出的话需要使用 getParameterMap() ,或者是一条数据一条数据去获取,但都是通过键值对的形式去获取的。

public class BODHandlerAdapter {
    public BODModelAndView handle(HttpServletRequest req,
                                  HttpServletResponse resp,
                                  BODHandlerMapping handler) throws InvocationTargetException, IllegalAccessException {
        Method method = handler.getMethod();
        //  填充参数
        Map<String, Integer> paramIndex = new HashMap<>();
        Annotation[][] annotations = method.getParameterAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            for (Annotation annotation : annotations[i]) {
                if (!(annotation instanceof BODRequestParam)) { continue; }
                String paramName = ((BODRequestParam) annotation).name();
                if (!"".equals(paramName.trim())) {
                    paramIndex.put(paramName, i);
                }
            }
        }
        //  获取参数位置
        Class<?>[] parameterTypes = method.getParameterTypes();
        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> type = parameterTypes[i];
            if (type == HttpServletRequest.class || type == HttpServletResponse.class) {
                paramIndex.put(type.getName(), i);
            }
        }
        //  设置参数详情
        Object[] params = new Object[parameterTypes.length];
        Map<String, String[]> parameterMap = req.getParameterMap();
        for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
            if (!paramIndex.containsKey(param.getKey())) { continue; }
            String string = Arrays.toString(param.getValue())
                    .replaceAll("\\[|\\]", "")
                    .replaceAll("\\s", "");
            params[paramIndex.get(param.getKey())] = string;
        }
        Object result = method.invoke(handler.getController(), params);
        if (result == null || result instanceof Void) { return null; }
        if (handler.getMethod().getReturnType() == BODModelAndView.class) {
            return (BODModelAndView) result;
        }
        return null;
    }
}

 4.3、视图解析

 在源码中我们可以通过 ModelAndView 这样的一个对象去渲染我们的结果页面,里面除了可以指定具体的显示页面之外,还可以携带参数去动态展示。而动态展示的方法都是替换,也就是说我们可以在页面中定义相关的参数匹配,然后在渲染页面时将具体的参数进行替换,这样就能达到结果的展示。

 Spring MVC 中使用的是 ${} 作为标志,我们这里就使用 !{} 作为标志,具体的步骤就是得到处理的返回结果后,如果是 BODModelAndView 对象,我们就去具体的模板文件中去找需要进行渲染的静态页面,然后加载到内存中,按行返回客户端,在这个过程中如果匹配到自定义的标识,就将具体的数据取出去替换,这样就达到动态的结果渲染。

public class BODView {
    private File viewFile;

    public BODView(File file) {
        this.viewFile = file;
    }

    /**
     * 渲染页面
     * @param model 渲染数据
     * @param req
     * @param resp
     * @throws IOException
     */
    public void render(Map<String,?> model, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        StringBuilder builder = new StringBuilder();
        RandomAccessFile accessFile = new RandomAccessFile(this.viewFile, "r");
        String line = null;
        while (null != (line = accessFile.readLine())) {
            line = new String(line.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);

            Pattern pattern = Pattern.compile("!\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
            Matcher matcher = pattern.matcher(line);
            while (matcher.find()) {
                String paramName = matcher.group();
                paramName = paramName.replaceAll("!\\{|\\}", "");
                Object paramValue = model.get(paramName);
                if (paramValue == null) { continue; }
                line = matcher.replaceFirst(paramValue.toString());
                matcher = pattern.matcher(line);
            }
            builder.append(line);
        }
        resp.setCharacterEncoding("utf8");
        resp.getWriter().write(builder.toString());
    }
}