美文网首页
java自定义注解及日志输出

java自定义注解及日志输出

作者: 博尔克斯 | 来源:发表于2020-10-25 22:40 被阅读0次

    本质是继承Annotation接口,是一种特殊的注释,解析一个类或方法的注解有两种方式,一种是编译器直接扫描,另一种是运行期反射:
    编译器扫描:如@Override,这种情况只适用于那些编译器已经熟知的注解类,JDK内置的几个注解

    public interface Override extends Annotation {
    }
    

    四个元注解

    修饰注解的注解,有下面四个

    @Target:注解的作用目标
    @Retention:注解的生命周期
    @Documented:注解是否应当被包含在 JavaDoc 文档中
    @Inherited:是否允许子类继承该注解

    @Target: 指明被修饰的注解最终作用目标是谁,可以传入多个,定义如下
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Target {
        // 指定修饰类型,ElementType定义在下面
        ElementType[] value();
    }
    

    @Target(value = {ElementType.FIELD}) 表示只能作用域成员属性字段上
    有以下这些类型:
    ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
    ElementType.FIELD:允许作用在属性字段上
    ElementType.METHOD:允许作用在方法上
    ElementType.PARAMETER:允许作用在方法参数上
    ElementType.CONSTRUCTOR:允许作用在构造器上
    ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
    ElementType.ANNOTATION_TYPE:允许作用在注解上
    ElementType.PACKAGE:允许作用在包上

    @Retention 用于指明当前注解的生命周期,定义如下
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Retention {
        // 传入类型为RetentionPolicy,定义在下面
        RetentionPolicy value();
    }
    

    value 类型时RetentionPolicy,如
    @Retention(value = RetentionPolicy.RUNTIME)
    RetentionPolicy.SOURCE:当前注解编译期可见,注解信息会被编译器丢弃,不会写入 class 文件
    RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件,在运行的时候不会被虚拟机读取
    RetentionPolicy.RUNTIME:永久保存,可以反射获取,既可以被虚拟机读取运行又保留在class文件中

    @Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Documented {
    }
    
    @Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Inherited {
    }
    

    内置三大注解

    @Override
    @Deprecated
    @SuppressWarnings

    @Override,没有任何属性,只用于方法上,编译结束后被丢弃
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    
    @Deprecated, 标记式注解,可以永久存在,作用是,标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
    public @interface Deprecated {
        String since() default "";
        boolean forRemoval() default false;
    }
    
    @SuppressWarnings 主要用来压制 java 的警告,有一个value属性就需要主动传值,表示压制的类型
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }
    
    

    自定义注解

    通过拦截器或AOP两种方式可以实现自定义注解,结合springboot使用。
    定义一个hello,target只修饰字段和方法,生命周期永久存活,可以在运行期反射获取:

    @Target(value = {ElementType.TYPE})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface World{
        String name();   // 定义一个名为name的String,没有默认值
        int age() default 18;  // age属性默认为18
        int[] array();  // 定义名为array的int数组
    }
    

    注解类型语法比较奇特,定义属性加上了(),注解在定义好了以后,使用的时候操作元素类型像在操作属性,解析的时候操作元素类型像在操作方法。举一个自定义注解的例子,一般在spring项目中常用的就是请求前后日志输出,下面定义一个输出日志的注解,用两种方式来使用注解

    @Target(value = {ElementType.FIELD, ElementType.METHOD})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface LogAccess{
        // 定义了只有一个元素,默认为"",属性为value时,传值可以直接使用,如@LogAccess("info")
        String value() default "";
    }
    
    切面使用注解

    直接对定义的注解做处理

    @Aspect
    @Component
    @Slf4j
    public class LogAccessAspect {
    
        /**
         * 1. @annotation 表示切点在注解上,括号里面是注解对应的全类名
         * 2. logPointCut() 切点名称,围绕切点产生一些问题
         */
        @Pointcut("@annotation(com.borkes.demo.annotation.LogAccess)")
        public void logPointCut() {
    
        }
    
        /**
         * 环绕通知,这里需要返回Object,否则请求执行后没有数据返回
         */
        @Around("logPointCut()")
        public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable{
            // 获取LogAccess注解标注方法的方法名和参数
            String methodName = joinPoint.getSignature().getName();
            Object[] param = joinPoint.getArgs();
    
            StringBuilder sb = new StringBuilder();
            for (Object o : param) {
                sb.append( o + ";");
            }
    
            log.info("[logAccess start]" + methodName +"|" + sb.toString());
    
            Object object;
            try {
                // 继续向下执行
                object = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                throw throwable;
            }
    
            log.info("[logAccess end]|" +methodName);
            return object;
        }
    }
    

    controller层对方法使用注解

        // post或get都可以
        @RequestMapping("/all")
        @LogAccess
        public String all() {
            return "hello world";
        }
    

    发起请求(当前controller增加了path @RequestMapping("/hello"))


    logAccess_req.jpg

    返回结果


    logAccess.jpg

    从结果可以看到已经打印出LogAccess进出的日志

    拦截器使用注解

    先实现拦截器接口

    public class SourceAccessInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            log.info("[HandlerInterceptor start]-----preHandle-----");
            HandlerMethod handlerMethod = (HandlerMethod)handler;
            LogAccess logAccess = handlerMethod.getMethod().getAnnotation(LogAccess.class);
            if (logAccess == null) {
                return true;
            }
            
            // 取到注解传入参数做处理,比如检查权限
            String value = logAccess.value();
            if ("check".equals(value)) {
                // 对注解标注的方法对应的请求做拦截处理
                //response.setContentType("application/json; charset=utf-8");
                //response.getWriter().print("log Access");
                return false;
            } else {
                return true;
            }
        }
    
        // 前后端分离,可以不处理下面两个方法
        // controller之后执行,DispatcherServlet视图渲染之前执行
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            log.info("[HandlerInterceptor ing]-----postHandle-----");
        }
    
        // DispatcherServlet视图渲染之后执行
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            log.info("[HandlerInterceptor end]-----afterHandle-----");
        }
    }
    

    再配置spring,实现WebMvcConfigurer,通过@Configuration注入配置

    @Configuration
    public class InterceptorTrainConfigurer implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**");
        }
    }
    

    同一个接口,在controller中增加了一条日志,用来区分controller执行前后输出


    interceptor.jpg

    controller层的日志确实在preHandle和postHandel中间输出,而且logAccess日志也是

    日志输出的两种方式

    上面两种用注解的方式的使用已经介绍完了,同时使用的都是用于日志输出,但由于注解都是对controller中的方法,切点比较局限,对于http请求的细节数据无法获取,所以需要用其他的方式来处理日志。

    切面切入到对应的controller层的所有方法

    Aop实现

    @Slf4j
    @Aspect  // 切面注解
    @Component  // 引入spring容器
    public class DemoAspect {
    
        // 最后的方法中两个点,表示所有参数都拦截
        // 定义通用切点
        @Pointcut("execution(public * com.borkes.demo.controller.HelloController.*(..))")
        public void log() {
        }
    
        /**
         * 请求前记录
         * @param joinPoint 请求相关节点
         */
        @Before("log()")
        public void dobefore(JoinPoint joinPoint) {
    
            // url 和 method
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
    
            log.info("[Aspect]ip={}, url={}", request.getRemoteAddr(), request.getRequestURL().toString());
            log.info("[Aspect]class_method={}, method={}, args:{}", joinPoint.getSignature(), request.getMethod(), joinPoint.getArgs());
        }
    
        @After("log()")
        public void doAfter() {
            log.info("[Aspect] after controller....");
        }
    
        /**
         * 返回的结果
         * @param object 请求结束返回对象
         */
        @AfterReturning(returning = "object", pointcut = "log()")
        public void doAfterReturning(Object object) {
            log.info("[Aspect] return:{}", object.toString());
        }
    }
    

    再请求看一下结果:


    aspect.jpg

    通过controller层我们可以获取到http层面的信息,对于controller层的方法和参数也可以拿到,之前定义注解都没有删除,注意自定义到方法的切片,拦截器,方法注解切片的日志输出顺序。

    Filter过滤器

    自定义切面实现已经可以对所有的controller实现日志输出,但执行顺序比较晚,灵活度也不够高。我们可以用过滤器来实现,而且用起来很简洁

    @Slf4j
    @Component  // 这里直接注入到容器(如果不使用bean的话),也可以在Application用注解`@ServletComponentScan`注入Servlet容器
    @WebFilter(filterName = "accessLog", urlPatterns = "/*")
    public class AccessFilter  implements Filter {
    
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
            // 处理postbody,如果不需要,可以直接用httpServletRequest
            RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest);
    
            long start = System.currentTimeMillis();
            log.info("[AccessFilter start]|method:{}, url:{}, query:{}, body:{}", httpServletRequest.getMethod(),
                    httpServletRequest.getRequestURL(), JSON.toJSONString(httpServletRequest.getParameterMap()), requestWrapper.getBody());
            filterChain.doFilter(requestWrapper, servletResponse);
            log.info("[AccessFilter end]|cost: {}", System.currentTimeMillis() - start);
        }
    
        @Override
        public void destroy() {
    
        }
    }
    

    单独处理post数据

    @Slf4j
    public class RequestWrapper extends HttpServletRequestWrapper {
    
        private final byte[] body;
    
        public RequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            int offset;
            byte[] temp = new byte[1024];
            InputStream inputStream = request.getInputStream();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    
            while((offset = inputStream.read(temp)) > -1) {
                byteArrayOutputStream.write(temp, 0, offset);
            }
            byteArrayOutputStream.flush();
            body = byteArrayOutputStream.toByteArray();
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
            return new ServletInputStream() {
                @Override
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }
                @Override
                public boolean isFinished() {
                    return false;
                }
                @Override
                public boolean isReady() {
                    return false;
                }
                @Override
                public void setReadListener(ReadListener readListener) {
                }
            };
        }
    
        public String getBody() {
            return new String(this.body);
        }
    }
    

    再整体看一下输出


    filter.jpg

    会发现filter执行非常靠前,如果要统计接口执行耗时,用filter能更精确

    总结

    1.过滤器,拦截器,切面是一个洋葱模型,开始结束和栈相似,最先开始输出start的也是最后输出end
    2.执行顺序 过滤器 -> 拦截器 -> 切片(多个顺序可自定义) -> controller

    参考:
    https://www.jianshu.com/p/a7bedc771204
    https://juejin.im/post/6844903925628272653

    相关文章

      网友评论

          本文标题:java自定义注解及日志输出

          本文链接:https://www.haomeiwen.com/subject/dwxomktx.html