Java API简单实现Spring aop/ioc

mini-spring

作为开发人员,Spring框架的应用面和频率,想必不用过多赘述,而框架的中的控制反转(IOC)和面向切面编程(AOP)思想也一直会伴随整个开发生涯。 如果想深刻理解Spring,那就需要去钻研源码,但是源码又太难看懂了,怎么办? 没关系,今天我们使用 JDK API 实现了一个乞丐版 IOC/AOP 框架 mini-spring.

简介

使用 Java API 以及内嵌 Tomcat 服务器写了一个乞丐级 IOC/AOP web 框架。实现了 `@Controller`、`@AutoWired`、`@Component` 、`@Pointcut`、`@Aspect`、`@Before`、`@After` 等 Spring 常用注解。可实现简单的访问 uri 映射,控制反转以及不侵入原代码的面向切面编程。 

若想实现该项目,首先需要已经掌握了基础的项目构建、反射、注解,以及 JDK 动态代理知识

模块组成

项目由两个模块组成,一个是框架本身的模块,实现了框架的 IOC/AOP 等功能:

aop包中是After等注解的定义接口,以及动态代理辅助类;

bean包中是两个注解定义,以及BeanFactory这个 Bean 工厂,其中包含了类扫描和 Bean 的初始化的代码;

core包是一个ClassScanner类扫描工具类;

starter包是一个框架的启动与初始化类;

web/handler包中是 uri 请求的处理器的收集与管理,如查找@Controller注解修饰的类中的@RequestMapping注解修饰的方法,用来响应对应 uri 请求。

web/mvc包定义了与 webMVC 有关的三个注解;

web/server包中是一个嵌入式 Tomcat 服务器的初始化类;

web/servlet包中是一个请求分发器,重写的service()方法定义使用哪个请求处理器来响应浏览器请求;

framework 实现流程

启动 tomcat 服务

public void startServer() throws LifecycleException {

        tomcat = new Tomcat();

        tomcat.setPort(8080);

        tomcat.start();

        // new 一个标准的 context 容器并设置访问路径;

        // 同时为 context 设置生命周期监听器。

        Context context = new StandardContext();

        context.setPath("");

        context.addLifecycleListener(new Tomcat.FixContextListener());

        // 新建一个 DispatcherServlet 对象,这个是我们自己写的 Servlet 接口的实现类,

        // 然后使用 `Tomcat.addServlet()` 方法为 context 设置指定名字的 Servlet 对象,

        // 并设置为支持异步。

        DispatcherServlet servlet = new DispatcherServlet();

        Tomcat.addServlet(context, "dispatcherServlet", servlet)

                .setAsyncSupported(true);

        // Tomcat 所有的线程都是守护线程,

        // 如果某一时刻所有的线程都是守护线程,那 JVM 会退出,

        // 因此,需要为 tomcat 新建一个非守护线程来保持存活,

        // 避免服务到这就 shutdown 了

        context.addServletMappingDecoded("/", "dispatcherServlet");

        tomcat.getHost().addChild(context);

        Thread tomcatAwaitThread = new Thread("tomcat_await_thread") {

            @Override

            public void run() {

                TomcatServer.this.tomcat.getServer().await();

            }

        };

        tomcatAwaitThread.setDaemon(false);

        tomcatAwaitThread.start();

    }

如果暂时不理解也没关系,不影响框架学习,我只是为了玩一玩内嵌 tomcat,完全可以自己实现一个乞丐版的网络服务器的。

这里使用的是我们自定义的 Servlet 子类 DispatcherServlet 对象,该类重写了service()方法,代码如下:

@Override

    public void service(ServletRequest req, ServletResponse res) throws IOException {

        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {

            // 从所有的 MappingHandler 中逐一尝试处理请求,

            // 如果某个 handler 可以处理(返回true),则返回即可

            try {

                if (mappingHandler.handle(req, res)) {

                    return;

                }

            } catch (IllegalAccessException e) {

                e.printStackTrace();

            } catch (InvocationTargetException e) {

                e.printStackTrace();

            }

        }

        res.getWriter().println("failed!");

    }

HandlerManager 和 MappingHandler 处理器后面会讲,这里先不展开。至此,tomcat 服务器启动完成;

扫描类

扫描类是通过这句代码完成的:

// 扫描类

List<Class<?>> classList = ClassScanner.scannerCLasses(cls.getPackage().getName());

ClassScanner.scannerCLasses方法实现如下:

public static List<Class<?>> scannerCLasses(String packageName)

            throws IOException, ClassNotFoundException {

        List<Class<?>> classList = new ArrayList<>();

        String path = packageName.replace(".", "/");

        // 线程上下文类加载器默认是应用类加载器,即 ClassLoader.getSystemClassLoader();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // 使用类加载器对象的 getResources(ResourceName) 方法获取资源集

        // Enumeration 是古老的迭代器版本,可当成 Iterator 使用

        Enumeration<URL> resources = classLoader.getResources(path);

        while (resources.hasMoreElements()) {

            URL url = resources.nextElement();

            // 获取协议类型,判断是否为 jar 包

            if (url.getProtocol().contains("jar")) {

                // 将打开的 url 返回的 URLConnection 转换成其子类 JarURLConnection 包连接

                JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();

                String jarFilePath = jarURLConnection.getJarFile().getName();

                // getClassesFromJar 工具类获取指定 Jar 包中指定资源名的类;

                classList.addAll(getClassesFromJar(jarFilePath, path));

            } else {

                // 简单起见,我们暂时仅实现扫描 jar 包中的类

                // todo

            }

        }

        return classList;

    }

    private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {

          List<String> classNames = Collections.list(new JarFile(jarFilePath).entries()).stream()

                .map(JarEntry::getName)

                .filter(entryName -> entryName.startsWith(path) && entryName.endsWith(".class"))

                .map(entryName -> entryName.replace("/", ".").substring(0, entryName.length() - 6))

                .collect(Collectors.toList());

        List<Class<?>> classes = new ArrayList<>();

        // 使用类的全限定类名初始化类,并将类对象保存

        try {

            for (String className : classNames) classes.add(Class.forName(className));

        } catch (ClassNotFoundException e) {

            e.printStackTrace();

        }

        return classes;

    }

初始化Bean工厂

这部分是最重要的,IOC 和 AOP 都在这里实现。

代码如下

public class BeanFactory {

    private static final Map<Class<?>, Object> beans = new ConcurrentHashMap<>();

    /**

    * 带有 @AutoWired 注解修饰的属性的类

    */

    private static final Set<Class<?>> beansHasAutoWiredField = Collections.synchronizedSet(new HashSet<>());

    public static Object getBean(Class<?> cls) {

        return beans.get(cls);

    }

    /**

    * 根据类列表 classList 来查找所有需要初始化的类并放入 Component 工厂,

    * 并且处理类中所有带 @AutoWired 注解的属性的依赖问题。

    */

    public static void initBean(List<Class<?>> classList) throws Exception {

        // 因为类定义后续处理类中 @RequestMapping 注解生成处理器时还要使用,

        // 因此这里要创建新容器,不能修改原引用

        List<Class<?>> classesToCreate = new ArrayList<>(classList);

        // 被 @Aspect 注解的切面类

        List<Class<?>> aspectClasses = new ArrayList<>();

        for (Class<?> aClass : classesToCreate) {

            if (aClass.isAnnotationPresent(Aspect.class)) {

                aspectClasses.add(aClass);

            } else {

                createBean(aClass);

            }

        }

        // 使用动态代理处理AOP

        resolveAOP(aspectClasses);

        // 有的类中某个属性已经通过 @AutoWired 注入了旧的被代理的对象,重新创建它们

        for (Class<?> aClass : beansHasAutoWiredField) {

            createBean(aClass);

        }

    }

    static Function<String, String> takeToBrackets = (strWithBrackets) -> strWithBrackets.substring(0, strWithBrackets.indexOf("("));

    /**

    * 通过 Class 对象创建实例

    *

    * @param aClass 需要创建实例的 Class 对象

    */

    private static void createBean(Class<?> aClass) throws IllegalAccessException, InstantiationException {

        // 只处理 @Component / @Controller 注解的类

        if (!aClass.isAnnotationPresent(Component.class)

                && !aClass.isAnnotationPresent(Controller.class)) {

            return;

        }

        // 初始化对象

        Object bean = aClass.newInstance();

        // 遍历类中所有定义的属性,如果属性带有 @AutoWired 注解,则需要注入对应依赖

        for (Field field : aClass.getDeclaredFields()) {

            if (!field.isAnnotationPresent(AutoWired.class)) {

                continue;

            }

            // 将需要注入其他 Bean 的类保存起来,因为等 AOP 代理类生成之后,需要更新它们

            BeanFactory.beansHasAutoWiredField.add(aClass);

            Class<?> fieldType = field.getType();

            field.setAccessible(true);

            if (fieldType.isInterface()) {

                // 如果依赖的类型是接口,则查询其实现类,

                // class1.isAssignableFrom(class2) = true; class1 可以从 class2 赋值,代表class2是class1类型,可分配class2对象给class1

                for (Class<?> key : BeanFactory.beans.keySet()) {

                    if (fieldType.isAssignableFrom(key)) {

                        fieldType = key;

                        break;

                    }

                }

            }

            field.set(bean, BeanFactory.getBean(fieldType));

        }

        // todo 这里可能AutoWired注入失败,例如存在循环依赖,或者bean工厂中根本不存在,目前暂时先不处理

        beans.put(aClass, bean);

    }

    /**

    * 对于所有被 @Aspect 注解修饰的类,

    * 遍历他们定义的方法,处理 @Pointcut、@Before 以及 @After 注解

    */

    private static void resolveAOP(List<Class<?>> aspectClasses)

            throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        for (Class<?> aClass : aspectClasses) {

            Method before = null; // 前置动作

            Method after = null; // 后置动作

            String method = null; // 切入方法

            Object target = null;

            String pointcutName = null;

            // 初始化对象,简单起见,这里先假定每一个代理类,

            // 并且最多只有一个切点,一个前置以及一个后置处理器,所以我们也必需先处理 pointcut,再解析before和after方法

            Object bean = aClass.newInstance();

            Method[] methods = aClass.getDeclaredMethods();

            for (Method m : methods) {

                if (m.isAnnotationPresent(Pointcut.class)) {

                    // com.caozhihu.demo.Rapper.rap()

                    String pointcutValue = m.getAnnotation(Pointcut.class).value();

                    // 截取全限定类名

                    String classStr = pointcutValue.substring(0, pointcutValue.lastIndexOf("."));

                    target = Thread.currentThread().getContextClassLoader().loadClass(classStr).newInstance();

                    method = pointcutValue.substring(pointcutValue.lastIndexOf(".") + 1);

                    pointcutName = m.getName();

                }

            }

            // 如果没有切点名,则返回

            if (pointcutName == null) continue;

            for (Method m : bean.getClass().getDeclaredMethods()) {

                if (m.isAnnotationPresent(Before.class)) {

                    String value = m.getAnnotation(Before.class).value();

                    if (takeToBrackets.apply(value).equals(pointcutName)) {

                        before = m;

                    }

                } else if (m.isAnnotationPresent(After.class)) {

                    String value = m.getAnnotation(After.class).value();

                    if (takeToBrackets.apply(value).equals(pointcutName)) {

                        after = m;

                    }

                }

            }

            // 获取代理对象并更新 bean 工厂

            Object proxy = new ProxyDyna().createProxy(bean, before, after,

                    target, takeToBrackets.apply(method));

            BeanFactory.beans.put(target.getClass(), proxy);

        }

    }

}

这里简单说下处理逻辑。

首先通过遍历上一步类扫描获得类的 Class 对象集合,将被@Aspect注解的类保存起来,然后初始化其他被@Component和@Controller注解的类,并处理类中被@AutoWired注解的属性,将目标引用对象注入(设置属性的值)到类中,然后将初始化好的对象保存到 Bean 工厂。到这里,控制反转就实现好了。

接下来是处理被@Aspect注解的类,并解析他们中被@Pointcut、@Before和@After注解的方法,使用 JDK 动态代理生成代理对象,并更新 Bean 工厂。

注意,在处理被@Aspect注解的类之前,Bean 工厂中的对象依赖已经设置过了就旧的 Bean,更新了 Bean 工厂中的对象后,需要通知依赖了被更新对象的对象重新初始化。

例如对象 A 依赖对象 B,即 A 的类中有一句

@AutoWired

B b;

同时,一个切面类中的切点@Pointcut的值指向了 B 类对象,然后他像 Bean 工厂更新了 B 对象,但这时 A 中引用的 B 对象,还是之前的旧 B 对象。

这里我的解决方式是,将带有@AutoWired属性的类保存起来,处理好 AOP 关系之后,再次初始化这些类,这样他们就能从 Bean 工厂获得新的已经被代理过的对象了。

至于如何使用 JDK 动态代理处理 AOP 关系的,请参考 GitHub ProxyDyna 类 中代码,总的来说是,定义一个ProxyDyna类实现InvocationHandler接口,然后实现invoke()方法即可,在invoke()方法中处理代理增强逻辑。

然后获取对象的时候,使用Proxy.newProxyInstance()方法而不是直接 new,如下:

Proxy.newProxyInstance(target.getClass().getClassLoader(),

                target.getClass().getInterfaces(), this);

初始化Handler

HandlerManager 类中调用parseHandlerFromController()方法来遍历处理所有的已扫描到的类,来初始化 MappingHandler 对象,方法代码如下:

private static void parseHandlerFromController(Class<?> aClass) {

        Method[] methods = aClass.getDeclaredMethods();

        // 只处理包含了 @RequestMapping 注解的方法

        for (Method method : methods) {

            if (method.isAnnotationPresent(RequestMapping.class)) {

                // 获取赋值 @RequestMapping 注解的值,也就是客户端请求的路径,注意,不包括协议名和主机名

                String uri = method.getDeclaredAnnotation(RequestMapping.class).value();

                List<String> params = new ArrayList<>();

                for (Parameter parameter : method.getParameters()) {

                    if (parameter.isAnnotationPresent(RequestParam.class)) {

                        params.add(parameter.getAnnotation(RequestParam.class).value());

                    }

                }

                // List.toArray() 方法传入与 List.size() 恰好一样大的数组,可以提高效率

                String[] paramsStr = params.toArray(new String[params.size()]);

                MappingHandler mappingHandler = new MappingHandler(uri, aClass, method, paramsStr);

                HandlerManager.mappingHandlerList.add(mappingHandler);

            }

        }

    }

MappingHandler 对象表示如何处理一次请求,包括请求 uri,应该调用的类,应该调用的方法以及方法参数。

如此,在 MappingHandler 的handle()方法中处理请求,直接从 Bean 工厂获取指定类对象,从 response 对象中获取请求参数值,使用反射调用对应方法,并接收方法返回值输出给浏览器即可。

再回顾我们启动 tomcat 服务器时指定运行的 servlet:

@Override

    public void service(ServletRequest req, ServletResponse res) throws IOException {

        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {

            // 从所有的 MappingHandler 中逐一尝试处理请求,

            // 如果某个 handler 可以处理(返回true),则返回即可

            try {

                if (mappingHandler.handle(req, res)) {

                    return;

                }

            } catch (IllegalAccessException e) {

                e.printStackTrace();

            } catch (InvocationTargetException e) {

                e.printStackTrace();

            }

        }

        res.getWriter().println("failed!");

    }

一目了然,其service()方法只是遍历所有的 MappingHandler 对象来处理请求而已。

框架使用

测试使用 IOC 和 AOP 功能。这里以定义一个 /rap 路径举例,

1. 定义Controller

@Controller

public class RapController {

    @AutoWired

    private Rap rapper;

    @RequestMapping("/rap")

    public String rap() {

        rapper.rap();

        return "CXK";

    }

}

RapController 从 Bean 工厂获取一个 Rap 对象,访问 /rap 路径是,会先执行该对象的rap()方法,然后返回 "CXK" 给浏览器。

2. 定义 Rap 接口及其实现类

public interface Rap {

    void rap();

}

// ----another file----

@Component

public class Rapper implements Rap {

    public void rap() {

        System.out.println("CXK rapping...");

    }

}

接口一定要定义,否则无法使用 AOP,因为我们使用的是 JDK 动态代理,只能代理实现了接口的类(原理是生成一个该接口的增强带向)。Spring 使用的是 JDK 动态代理和 CGLIB 两种方式,CGLIB 可以直接使用 ASM 等字节码生成框架,来生成一个被代理对象的增强子类。

使用浏览器访问http://localhost:8080/rap,即可看到 IDE 控制台输出CXK rapping...,可以看到,@AutoWired注解成功注入了对象。

但如果我们想在 rap 前面先 唱、跳,并且在 rap 后面打篮球,那么就需要定义织面类来面向切面编程。

定义一个RapAspect类如下:

@Aspect

@Component

public class RapAspect {

    // 定义切点,spring的实现中,

    // 此注解可以使用表达式 execution() 通配符匹配切点,

    // 简单起见,我们先实现明确到方法的切点

    @Pointcut("Rapper.rap()")

    public void rapPoint() {

    }

    @Before("rapPoint()")

    public void singAndDance() {

        // 在 rap 之前要先唱、跳

        System.out.println("first,singing <chicken is too beautiful>.");

        System.out.println("and the chicken monster is dancing now.");

    }

    @After("rapPoint()")

    public void basketball() {

        // 在 rap 之后别忘记了篮球

        System.out.println("oh! Don't forget my favorite basketball.");

    }

}

织面类 RapAspect 定义了切入点以及前置后置通知等,这样 RapController 中使用@AutoWired注解引入的 Rap 对象,会被替换为增强的 Rap 代理对象,如此,我们无需改动 RapController 中任何一处代码,就实现了在rap()方法前后执行额外的代码(通知)。

增加 RapAspect 后,再次访问会在 IDE 控制台输出:

first,singing <chicken is too beautiful>.

and the chicken monster is dancing now.

CXK rapping...

oh! Don't forget my favorite basketball.

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容