造一个方形的轮子8--添加Aop支持

造一个方形轮子文章目录:造一个方形的轮子

01、解决遗留问题

上一篇的最后说了几个问题,没有处理500、没有处理from表单、没有处理文件上传,文件上传和表单可以算是同一个问题了,偷个懒就先不解决了,思路的话就是把文件单独封装一个接收对象放内存或临时缓存,from表单可以按参数名封装接收参数bean。这里先解决一下500的问题。

程序触发500的情况是在处理Controller方法及后续调用方法的时候产生的,也就是说只要在DispatcherServlet里catch一下invoke()方法,记录一下日志并设置一下返回结果就可以了。

@Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 解析url
        String contextPath = req.getContextPath();
        String httpMethod = req.getMethod();
        String uri = req.getRequestURI();
        // 匹配到对应的controller
        String controllerKey = httpMethod.toLowerCase()+":"+uri.replace(contextPath, "");
        ControllerObject controllerObject = beansMap.getController(controllerKey);
        // 如果没有匹配,返回404
        if(controllerObject == null){
            resp.sendError(404);
        } else {
            try {
                // 执行对应方法
                Object obj = controllerObject.invoke(req);
                // 处理返回结果
                String json;
                if (obj instanceof String) {
                    json = (String) obj;
                } else {
                    json = JSON.toJSONString(obj);
                    resp.setHeader("content-type", "application/json;charset=UTF-8");
                }
                log.info("http request path:" + controllerKey);
                log.info("exec method :" + controllerObject.getMethod().getName());
                log.info("response:" + json);
                resp.getWriter().print(json);
            } catch (Exception e){
                log.error("Controller invoke error! controllerKey:{}", controllerKey);
                resp.sendError(500, "Internal Server Error");
            }
        }
    }

日志输出错误的请求uri,并返回500错误。

02、Aop功能整理

在开始开发Aop功能之前先来回顾一下Aop的知识点,一般来说,Aop指的就是面向切面编程,也就是程序中找出一个点或一条线(由多个点组成)来,针对性的做程序处理,一般用来做统一鉴权、日志记录之类的。

上边是从使用的角度考虑,当然如果开发的话要考虑的还是有点多的,我只是简单实现了一下,而且遗留问题还不少,只是基本能跑了,还有N多种情况是没有处理的,只有真正写完才觉得Spring真的很赞,它把那么多基本功能提供出来,让开发人员能够专注在业务开发上。

事先声明我实现的方式依然是简单粗暴不要拿来和其它成熟框架做对比,当然觉得有问题的也欢迎指正,写的仓促我已知的问题已经不少,大家能提出来一起讨论也好。

说一下设计Aop功能的思路:

1、定义Aop相关注解,并在初始化Bean之前先加载Aop切面

2、初始化Bean阶段判断package是否有匹配的切面,如果有则生成代理类,保存原始对象和代理对象

3、处理DI阶段向原始对象里注入代理(原始)对象(这句话后边结合代码详细说)

4、程序调用时如果是代理类,在代理类里判断当前调用方法是否有匹配的切面规则,有则按规则执行切面

这个思路可能不太好理解,还是结合代码看一下吧。

03、添加注解

新增三个注解,一个作用在类上的Aspect标识当前类为切面类,一个Pointcut标记切入点,和一个Before标记方法在切入点的执行动作,这里只实现了一个Before对应的After、 Around都类似,就没都实现。

Aspect.java:

package com.jisuye.annotations.aop;
// import ...
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Aspect {
}

Before.java:

package com.jisuye.annotations.aop;
// import ...
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Before {
    String value() default "";
}

Pointcut.java:

package com.jisuye.annotations.aop;
// import ...
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pointcut {
    String value() default "";
}

04、添加代理类、切面对象、HTTP请求上下文

切面对象也是放在容器里的,因为后边代理对象里会使用,切面对象保存的是切面的规则信息,及对应的切面类实例,动作方法等信息。

AspectObject.java:

package com.jisuye.core;
// import ...
/**
 * AspectObject 封装
 * @author ixx
 * @date 2019-08-03
 */
public class AspectObject {
    /**
     * 包路径
     */
    private String packagePath;

    /**
     * 方法切面规则
     */
    private List<String> methodEx = new ArrayList<>();

    /**
     * 保存Aspect对象
     */
    private Object AspectBean;

    /**
     * 执行方法集
     * key : methodEx
     * value : AspectBean.method()
     */
    private Map<String, Method> methodMap = new HashMap<>();

    /**
     * 类匹配
     */
    private String className;

    /**
     * 返回类型
     */
    private String retClass;

    /**
     * 切面类型(before、after...)
     */
    private String type;

    public void setMethodEx(String methodEx) {
        this.methodEx.add(methodEx);
    }

    public void setMethodMap(String key, Method method) {
        this.methodMap.put(key, method);
    }
    //geter seter ...
}

代理类这里只使用jdk自代的动态代理,也就是说代理的类必须实现接口。

invoke内部执行过程:

1、根据package从容器中获取对应的AspectObject

2、判断当前类、方法、返回类型是否有匹配的切入点

3、如果有,获取使用当前切入点的方法,并按逻辑执行

SquareProxyHandler.java:

package com.jisuye.core;
// import ...
/**
 * Square 代理类,使用JDK自带代理,只支持接口方式
 * @author ixx
 * @date 2019-08-03
 */
public class SquareProxyHandler implements InvocationHandler {

    private Object obj;

    public SquareProxyHandler(Object obj){
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 处理Aop 逻辑
        String packageClass = obj.getClass().getName();
        // 包名
        String packageStr = packageClass.substring(0, packageClass.lastIndexOf("."));
        // 类名
        String classStr = packageClass.substring(packageStr.length()+1);
        // 获取AspectObject
        AspectObject aspectObject = BeansMap.getAop(packageStr);
        if(aspectObject != null){
            // 匹配类名,方法名,返回值类型  看是否需符合切面规则
            Method aspectMethod = verification(method, classStr, aspectObject);
            if(aspectMethod != null){
                // 前置
                if(aspectObject.getType().equals("before")){
                    aspectMethod.invoke(aspectObject.getAspectBean(), null);
                    Object o = method.invoke(obj, args);
                    return o;
                }
            }
        }
        Object o = method.invoke(obj, args);
        return o;
    }

    /**
     * 验证是否符合切面规则
     * @param method 当前要执行的方法
     * @param aspectObject 切面对象
     * @return
     */
    private Method verification(Method method, String className, AspectObject aspectObject){
        // 判断类名
        if(!Pattern.matches(aspectObject.getClassName(), className)){
            return null;
        }
        // 判断返回值类型
        if(!Pattern.matches(aspectObject.getRetClass(), method.getReturnType().getSimpleName())){
            return null;
        }
        // 判断方法名
        List<String> list = aspectObject.getMethodEx();
        String methodName = method.getName()+"(..)";
        for (String s : list) {
            if(Pattern.matches(s, methodName)){
                Method aspectMethod = aspectObject.getMethodMap().get(s);
                return aspectObject.getMethodMap().get(aspectMethod.getName());
            }
        }
        return null;
    }
}

HTTP请求上下文的类,是为了保存当前请求线程的Request 及Response 方便在后续方法中使用。

RequestContextHolder.java:

package com.jisuye.core;
// import ...
/**
 * 保存当前线程对应Request请求
 * @author ixx
 * @date 2019-07-27
 */
public class RequestContextHolder {
    private static ThreadLocal<HttpServletRequest> requestThreadLocal;

    private static ThreadLocal<HttpServletResponse> responseThreadLocal;


    public static HttpServletRequest getRequest() {
        if(requestThreadLocal == null){
            return null;
        }
        return requestThreadLocal.get();
    }

    public static void setRequest(HttpServletRequest request) {
        requestThreadLocal = ThreadLocal.withInitial(() -> request);
    }

    public static HttpServletResponse getResponse() {
        if(responseThreadLocal == null){
            return null;
        }
        return responseThreadLocal.get();
    }

    public static void setResponse(HttpServletResponse response) {
        responseThreadLocal = ThreadLocal.withInitial(() -> response);
    }

    public static void init(HttpServletRequest request, HttpServletResponse response){
        requestThreadLocal = ThreadLocal.withInitial(() -> request);
        responseThreadLocal = ThreadLocal.withInitial(() -> response);
    }
}

05、修改容器初始化逻辑

BeansMap中添加aopMap:

public class BeansMap {
    // bean容器
    private static HashMap<String, BeanObject> beans = new HashMap<>();

    // controller容器
    private static HashMap<String, ControllerObject> controllers = new HashMap<>();

    // aop容器
    private static HashMap<String, AspectObject> aops = new HashMap<>();

    public void putAop(String key, AspectObject aspectObject){
        aops.put(key, aspectObject);
    }

    public static AspectObject getAop(String key){
        return aops.get(key);
    }
    // ...
}

BeansInitUtil.init()方法中在处理IOC/DI之前先加载Aop切面保存到BeansMap.aops中,这块的代码有点长 我分段给出,完整代码可以去代码库中查找。

下边这段代码的逻辑是遍历目录,查找有@Aspect注解的类,并分析带@Pointcut注解的方法,做为切入点,以及使用该切入点的带@Before注解的方法做为动作执行方法,构造一下AspectObject对象,保存到BeansMap.aops中。

BeansInitUtil.java:

//......
    public static BeansMap init(Class clazz, BeansMap beansMap){
        String path = clazz.getResource("").getPath();
        log.info("===bean init path:{}", path);
        File root = new File(path);
        // 先加载aop类
        initAop(root, beansMap);
        // 处理控制反转
        initFile(root, beansMap);
        // 处理依赖注入
        initDI(beansMap);
        return beansMap;
    }
    private static void initAop(File file, BeansMap map){
        File[] fs = file.listFiles();
        for (File f : fs) {
            if(f.isDirectory()){
                // 递归目录
                initAop(f, map);
            } else {
                // 处理class
                loadAop(f, map);
            }
        }
    }
    private static void loadAop(File file, BeansMap map){
        log.info("load bean path:{}", file.getPath());
        try {
            Class clzz = Class.forName(getClassPath(file));
            Annotation[] annotations = clzz.getAnnotations();
            if (annotations.length > 0 && filterAspectAnnotation(annotations)) {
                Object obj = clzz.newInstance();
                Method[] methods = clzz.getMethods();
                for (Method method : methods) {
                    Annotation[] methodAnnotations = method.getAnnotations();
                    if(methodAnnotations != null && methodAnnotations.length > 0){
                        // 切点
                        if(methodAnnotations[0] instanceof Pointcut){
                            AspectObject aspectObject = BeansMap.getAop(method.getName());
                            if(aspectObject == null){
                                aspectObject = new AspectObject();
                                aspectObject.setAspectBean(obj);
                            }
                            String packageStr = setPointPackageAndMethod(((Pointcut)methodAnnotations[0]).value(), aspectObject, method);
                            map.putAop(packageStr, aspectObject);
                            map.putAop(method.getName(), aspectObject);
                        } else if(methodAnnotations[0] instanceof Before){
                            // Before 处理
                            String val = ((Before)methodAnnotations[0]).value();
                            val = val.substring(0, val.indexOf("("));
                            AspectObject aspectObject1 = BeansMap.getAop(val);
                            if(aspectObject1 == null){
                                aspectObject1 = new AspectObject();
                                aspectObject1.setAspectBean(obj);
                            }
                            aspectObject1.setType("before");
                            aspectObject1.setMethodMap(val, method);
                            map.putAop(val, aspectObject1);
                        }
                    }
                }
            }
        } catch (Exception e){
            log.error("load aop error!!", e);
        }
    }

    private static String setPointPackageAndMethod(String ex, AspectObject aspectObject, Method method){
        if(ex == null || ex.equals("")){
            throw new SquareException("Aop Acpect config is error, pointCut must value!");
        }
        ex = ex.replace("excution(", "");
        ex = ex.substring(0, ex.length()-1);
        String[] exs = ex.split(" ");
        if(exs.length != 3){
            throw new SquareException("Aop Acpect config is error!");
        } else {
            String packages = exs[2];
            String classStr = packages.replaceAll("\\.[a-zA-Z0-9\\*]*\\(.*\\)", "");
            String methods = packages.substring(classStr.length());
            methods = methods.substring(1).replace("*", ".*");
            String packageStr = classStr.replaceAll("\\.[a-zA-Z0-9\\*]*$", "");
            classStr = classStr.substring(packageStr.length());

            // 设置 包名 返回类型匹配 类名匹配 方法匹配
            aspectObject.setPackagePath(packageStr);
            aspectObject.setMethodEx(methods);
            aspectObject.setMethodMap(methods, method);
            aspectObject.setRetClass(exs[1].replace("*", ".*"));
            aspectObject.setClassName(classStr.substring(1).replace("*", ".*"));
            return packageStr;
        }
    }
//......

接下来看一下获取完Aop切面信息之后在哪里使用,修改BeansInitUtil.loadClass()方法,也就是初始化Bean的方法,原来方法里获取反射对象实例直接使用了clzz.newInstance(),现在要再加一个判断他是不是有匹配的切面信息的逻辑,如果符合某一个切面,则要生成一个代码类,然后保存原始对象及代理对象。

BeansInitUtil.java:

//...
    private static void loadClass(File file, BeansMap map){
        //...
                Object obj = clzz.newInstance();
                Object proxyObj = getInstance(clzz, obj);
                beanObject.setClass(clzz);
                beanObject.setObject(obj, proxyObj);
        //...
    }
/**
     * 获取反射实例,判断是否符合切面,是否需要做代理处理
     * @param clzz
     * @return
     */
    private static Object getInstance(Class clzz, Object obj) throws IllegalAccessException, InstantiationException {
        String packageStr = clzz.getPackage().getName();
        // 如果不符合切面规则,则直接反射生成 不使用代理
        if(BeansMap.getAop(packageStr) == null){
            return obj;
        }
        // 使用JDK动态代理
        Class[] interfaces = clzz.getInterfaces();
        if(interfaces == null || interfaces.length == 0){
            log.warn("{} Aop proxy class must implements interface!!", clzz.getName());
            return obj;
        }
        SquareProxyHandler squareProxyHandler = new SquareProxyHandler(obj);
        Object newProxyInstance = Proxy.newProxyInstance(interfaces[0].getClassLoader(), new Class[]{interfaces[0]}, squareProxyHandler);
        return newProxyInstance;
    }
//...

可以看到getInstance()方法返回的内容有可能是代理类,也有可能是传入对象本身,这里是做了兼容处理,还有beanObject.setObject()方法由原来的一个参数改成了两个参数,这是因为添加了一个代理对象,下边马上就能讲到为什么加这个对象。

BeanObject.java:

public class BeanObject {
    //...
    /**
     * 代理(实际)对象
     */
    private Object object;
    /**
     * 实际对象
     */
    private Object srcObj;
    
    public void setObject(Object obj, Object proxy) {
        this.srcObj = obj;
        this.object = proxy;
    }
    //...
}

正常来说我们原来使用的是反射生成的对象,现在添加了Aop直接替换为代理对象就可以了,但是这里有一个问题。在处理DI关系的时候,是不可以向代理对象里注入依赖的,好在代理对象也是包装的原始对象,也就是说我们把原始对象的依赖注入成功后,调用代理对象的方法也就不会出现空指针的问题了,看一下相关代码:

BeansInitUtil.java:

    //...
    private static void initDI(BeansMap map){
        //...
                    // 注入依赖
                    try {
                        field.setAccessible(true);
                        field.set(beanObject.getSrcObj(), bean.getObject());
                    } catch (IllegalAccessException e) {
                        log.error("Bean注入失败,field:{}", beanObject.getClassName()+"."+field.getName(), e);
                        throw new SquareException("Bean注入失败");
                    }
         //...
    }

主要的区别就是field.set()方法的对数,原来取的是beanObject.getObject()(代理对象)现在取的是beanObject.srcObj()(原始对象)

06、测试AOP

创建切面类

WebLogAspect.java:

package com.jisuye.service.aop;
// import ...
/**
 * 定义切面类,测试切面
 */
@Aspect
@Component
public class WebLogAspect {
    private static final Logger log = LoggerFactory.getLogger(WebLogAspect.class);
    /**
     * 测试com.jisuye.service.impl包下的所以类的abc开头方法(所有参数)添加AOP
     */
    @Pointcut("excution(public * com.jisuye.service.impl.*.abc*(..))")
    public void log(){}

    @Before("log()")
    public void doBefore(){
        HttpServletRequest request = RequestContextHolder.getRequest();
        if(request != null) {
            log.info("AOP exe... url:{}, params:{}", request.getRequestURI(), JSON.toJSONString(request.getParameterMap()));
        }
    }
}

修改Abc.java:

public interface Abc {
    int test(String name);
    int abc(String name);
    int abc2(String name);
}

添加了两个方法,AbcImpl里只是空实现就不放代码了,看一下TestController.java:

@Controller("/test")
public class TestController {

    @Resource
    private Abc abc;

    @Resource
    private JdbcTemplate jdbcTemplate;

    @GetMapping("/hello")
    public List<AbcEntity> test(@RequestParam("name") String name, @RequestParam("a") String age){
        List<AbcEntity> list = jdbcTemplate.select("select * from abc where name=?", AbcEntity.class, name);
        abc.test("ixx");
        return list;
    }
    @GetMapping("/abc")
    public String abc(){
        abc.abc("ixx");
        return "success";
    }

    @GetMapping("/abc2")
    public String abc2(){
        abc.abc2("ixx");
        return "success";
    }
    //...
}

添加调用abc及abc2的接口。

启动程序

调用接口:http://localhost:8888/abc/test/abc?name=ixx

查看控制台输出:

14:31:36.102 [http-nio-8888-exec-4] INFO com.jisuye.service.aop.WebLogAspect - AOP exe... url:/abc/test/abc, params:{"name":["ixx"]}
14:31:36.102 [http-nio-8888-exec-4] INFO com.jisuye.core.DispatcherServlet - http request path:get:/test/abc
14:31:36.102 [http-nio-8888-exec-4] INFO com.jisuye.core.DispatcherServlet - exec method :abc
14:31:36.102 [http-nio-8888-exec-4] INFO com.jisuye.core.DispatcherServlet - response:success

调用接口:http://localhost:8888/abc/test/abc2?name=ixx

查看控制台输出:

14:32:07.156 [http-nio-8888-exec-6] INFO com.jisuye.service.aop.WebLogAspect - AOP exe... url:/abc/test/abc2, params:{"name":["ixx"]}
14:32:07.156 [http-nio-8888-exec-6] INFO com.jisuye.core.DispatcherServlet - http request path:get:/test/abc2
14:32:07.156 [http-nio-8888-exec-6] INFO com.jisuye.core.DispatcherServlet - exec method :abc2
14:32:07.156 [http-nio-8888-exec-6] INFO com.jisuye.core.DispatcherServlet - response:success

调用接口:http://localhost:8888/abc/test/hello?name=ixx

查看控制台输出:

14:33:31.186 [http-nio-8888-exec-10] INFO com.jisuye.core.DispatcherServlet - http request path:get:/test/hello
14:33:31.186 [http-nio-8888-exec-10] INFO com.jisuye.core.DispatcherServlet - exec method :test
14:33:31.186 [http-nio-8888-exec-10] INFO com.jisuye.core.DispatcherServlet - response:[{"id":3,"name":"ixx"}.....

可以看到符合我们规则的方法调用都输出了AOP日志。

07、遗留问题

AOP的基本功能实现了,但问题点太多了,比如只实现了一个Before动作,没有处理方法参数,加入了aop后导致多了一遍目录遍历,而且BeansInitUtil类快看不下去了....下一篇之前找时候看看再梳理一下代码吧。

本篇代码地址: https://github.com/iuv/square/tree/square8

本文作者: ixx
本文链接: http://jianpage.com/2019/08/10/square8
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!

推荐阅读更多精彩内容