美文网首页
制作一个简单的代码校验器

制作一个简单的代码校验器

作者: guessguess | 来源:发表于2021-02-04 16:38 被阅读0次

    由于在公司的时候,发现口头上的代码规范,没有一个人遵守,于是乎。想着做一个什么东西来强制大家代码上的使用。
    我的想法很简单粗暴,规范有问题,容器无法启动,无法进行单元测试。这样子一来,只要有人没有按规范来,目的就达到了。
    考虑到的点如下。
    1.什么时候去做代码校验。
    我个人的想法是既然要在容器启动的时候去做这个事情,那就在容器刷新完成的时候去做这个事情。

    2.校验的代码通过什么机制去执行。
    这里我是偶然看书到的,插入式注解处理器,可以在执行javac指令的时候,去指定对应的注解处理器代码,这里的代码指的就是我们自己定义的校验代码。

    基于这2个点,所以需要利用到的东西有如下。看上去东西不是很多哈。
    1.注入式注解处理器。
    2.容器的监听器,用于在容器完成刷新的时候,进行代码校验。

    先准备一个容器工具类,用于获取容器本身。这是此前自己做的一个容器工具类,里面原理也提及到了,这里就不多说了,后续会使用到这个工具类。
    https://www.jianshu.com/p/d7b7f79cc94f

    容器监听器的创建

    1.首先创建一个容器监听器。我们只需要简单的实现容器监听器的接口,且注入到容器即可。

    @Component
    public class CustomApplicationContextEventListener implements ApplicationListener<ContextRefreshedEvent> {
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
        
        }
    }
    

    2.接下来,在实际运行中会有几个问题

    首先,Spring容器本身包含着若干个容器,如何知道主容器什么时候刷完呢?
    其次,在项目实际运行的过程中,包含了若干个环境,只有生产环境不需要去校验吧?
    代码已经解决了上面俩个问题。
    这里需要注意的是,需要进行强依赖,@DependsOn(value = "applicationContextUtils"),所以添加了注解。

    @Component
    @Slf4j
    @DependsOn(value = "applicationContextUtils")
    public class CustomApplicationContextEventListener implements ApplicationListener<ContextRefreshedEvent> {
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            // 某些类在注入进行实例化的过程中也会创建容器,并且进行刷新
            // 所以只有这样子判断,当AnnotationConfigServletWebServerApplicationContext刷新完,整个容器的刷新过程才结束
            boolean validateContextType = event
                    .getApplicationContext() instanceof AnnotationConfigServletWebServerApplicationContext;
            if (!validateContextType) {
                return;
            }
            // 生产环境的时候没必要进行校验.
            String active = ApplicationContextUtils.getActiveProfile();
            log.info("CustomApplicationContextEventListener onApplicationEvent active = " + active);
            if (Constants.PROD.equals(active)) {
                return;
            }
        }
    }
    

    为什么要这么写。

    2.1为什么只有AnnotationConfigServletWebServerApplicationContext刷新完,才是容器真正完成刷新,这里可以看看下面的源码, 从springboot程序启动的入口,可以很简单的发现这个东西。

    @SpringBootApplication
    @EnableFeignClients
    @EnableAspectJAutoProxy
    @ServletComponentScan
    @EnableConfigurationProperties
    public class IbsDataServiceApplication  {
    
        public static void main(String[] args) {
            SpringApplication.run(IbsDataServiceApplication.class, args);
        }
    }
    
    //最后看到内部的实现。
    public class SpringApplication {
        public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
            return run(new Class<?>[] { primarySource }, args);
        }
        public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
            return new SpringApplication(primarySources).run(args);
        }
        public ConfigurableApplicationContext run(String... args) {
            。。。
                //这里面我们只关心容器创建的类型。所以我将其他的代码都清空了。
                context = createApplicationContext();//307行
           。。。
        }
            
        //这个方法里面就已经告诉我们最后创建的主容器类型,这里我实际项目用的是web容器,对应的是SERVLET,那么对应的类便是DEFAULT_SERVLET_WEB_CONTEXT_CLASS
        protected ConfigurableApplicationContext createApplicationContext() {
            Class<?> contextClass = this.applicationContextClass;
            if (contextClass == null) {
                try {
                    switch (this.webApplicationType) {
                    case SERVLET:
                        contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                        break;
                    case REACTIVE:
                        contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                        break;
                    default:
                        contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
                    }
                }
                catch (ClassNotFoundException ex) {
                    throw new IllegalStateException(
                            "Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass",
                            ex);
                }
            }
            return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
        }
        //这便是最后容器的类。当这个容器刷新完成,才是所有容器都完成刷新。
        public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
                + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
    }
    

    2.2为什么要加强依赖,让工具类提前获取到容器。这样子就可以获取容器的相关信息,如运行环境。这样子就可以判断容器的运行环境,来决定要不要进行代码校验。

    这里还发现一个有趣的事情。ApplicationContextAwareProcessor这个类是用于处理ApplicationContextAware的子类,但是发现并没有被注入到容器,此外ApplicationContextAwareProcessor必须比ApplicationContextUtils更先实例化,否则如何利用ApplicationContextAwareProcessor去将容器注入到工具类中呢?看看源码,确实没有被注入。没被注入,那么什么时候实例化?从以下源码可以看出,ApplicationContextAwareProcessor在工厂准备前就已经实例化了,所以就可以保证
    ApplicationContextAwareProcessor必须比ApplicationContextUtils更先实例化了。如果不加强依赖的话,监听器的实例化必然优先于普通的实例。

    class ApplicationContextAwareProcessor implements BeanPostProcessor {
    }
    

    ApplicationContextAwareProcessor什么时候实例化的?在bean工厂的前置准备的时候。

    public abstract class AbstractApplicationContext extends DefaultResourceLoader
            implements ConfigurableApplicationContext {
        protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
            ...省略了,关键点在于第652行,直接实例化,同时将this,也就是容器设置到ApplicationContextAwareProcessor的成员变量中。后续如果实现了ApplicationContextAware接口的类,就可以获取到容器本身了。
            beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
            }
        }
    }
    
    class ApplicationContextAwareProcessor implements BeanPostProcessor {
    
        private final ConfigurableApplicationContext applicationContext;
    
        private final StringValueResolver embeddedValueResolver;
    
    
        /**
         * Create a new ApplicationContextAwareProcessor for the given context.
         */
        //实例化的使用,将容器注入。
        public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
            this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory());
        }
    
    
        @Override
        @Nullable
       //所有bean在初始化前,会执行该方法,所以这是一个切入点。只需要判断是不是ApplicationContextAware即可决定需不需要注入容器。
        public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
            AccessControlContext acc = null;
    
            if (System.getSecurityManager() != null &&
                    (bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
                            bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
                            bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
                acc = this.applicationContext.getBeanFactory().getAccessControlContext();
            }
    
            if (acc != null) {
                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                    invokeAwareInterfaces(bean);
                    return null;
                }, acc);
            }
            else {
                invokeAwareInterfaces(bean);
            }
    
            return bean;
        }
    
        private void invokeAwareInterfaces(Object bean) {
            if (bean instanceof Aware) {
                //如果是ApplicationContextAware接口的子类,则将容器设置进去即可
                if (bean instanceof ApplicationContextAware) {
                    ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
                }
            }
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) {
            return bean;
        }
    
    }
    
    

    插入式注解处理器

    1.实现

    @SupportedAnnotationTypes("*")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class NameCheckProcessor extends AbstractProcessor {
    
        private NameChecker nameChecker;
        @Override
        public void init(ProcessingEnvironment precessingEnv){
            super.init(precessingEnv);
            nameChecker = new NameChecker(precessingEnv);
        }
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            if(!roundEnv.processingOver()){
                for(Element element:roundEnv.getRootElements()){
                    nameChecker.checkNames(element);
                }
            }
            return false;
        }
    }
    
    public class NameChecker {
        private final Messager messager;
        NameCheckScanner nameCheckScanner = new NameCheckScanner();
        public NameChecker(ProcessingEnvironment precessingEnv) {
            this.messager = precessingEnv.getMessager();
        }
    
        public void checkNames(Element element) {
            nameCheckScanner.scan(element);
        }
        private class NameCheckScanner extends ElementScanner6<Void,Void>{
            @Override
            public Void visitType(TypeElement e,Void p){
                scan(e.getTypeParameters(),p);
                checkCamelCase(e,true);
                super.visitType(e,p);
                return null;
            }
    
            private void checkCamelCase(Element e, boolean b) {
                String name = e.getSimpleName().toString();
                boolean previousUpper = false;
                boolean conventional = true;
                int firstCodePoint = name.codePointAt(0);
                if(Character.isUpperCase(firstCodePoint)) {
                    previousUpper = true;
                    if (!b) {
                        messager.printMessage(Kind.ERROR, "首字母要小写",e);
                    }
                    return;
                }else if(Character.isLowerCase(firstCodePoint)){
                    if (b) {
                        messager.printMessage(Kind.ERROR, "首字母要大写",e);
                    }
                    return;
                }else{
                    int cp = firstCodePoint;
                    for(int i= Character.charCount(cp);i<name.length();i+=Character.charCount(cp)){
                        cp = name.codePointAt(i);
                        if(Character.isUpperCase(cp)){
                            if(previousUpper){
                                conventional = false;
                                break;
                            }
                            previousUpper =true;
                        }else {
                            previousUpper = false;
                        }
                    }
                    if(!conventional){
                        messager.printMessage(Kind.ERROR, "需要按照驼峰命名法",e);
                    }
                }
            }
    
            @Override
            public Void visitExecutable(ExecutableElement e, Void p){
                if(e.getKind()==METHOD) {
                    Name name = e.getSimpleName();
                    //不能与构造函数重名
                    if(name.contentEquals(e.getEnclosingElement().getSimpleName())){
                        messager.printMessage(Kind.ERROR,"不能与构造函数重名",e);
                        checkCamelCase(e,false);
                    }
                    super.visitExecutable(e, p);
                }
                return null;
            }
            @Override
            public Void visitVariable(VariableElement e, Void p){
                if(e.getKind()==ENUM_CONSTANT||e.getConstantValue()!=null||heuristicallyConstant(e)) {
                    checkAllCaps(e);
                }else{
                    checkCamelCase(e,false);
                }
                return null;
            }
    
            private void checkAllCaps(VariableElement e) {
                String name = e.getSimpleName().toString();
                boolean conventional = true;
                int firstCodePoint = name.codePointAt(0);
                if(!Character.isUpperCase(firstCodePoint)) {
                    conventional = false;
                }else{
                    boolean previousUppercore = false;
                    int cp = firstCodePoint;
                    for(int i= Character.charCount(cp);i<name.length();i+=Character.charCount(cp)){
                        cp = name.codePointAt(i);
                        if(cp==(int)'_'){
                            if(previousUppercore){
                                conventional = false;
                                break;
                            }
                            previousUppercore =true;
                        }else {
                            previousUppercore = false;
                            if(!Character.isUpperCase(cp)&&!Character.isDigit(cp)){
                                conventional =false;
                                break;
                            }
                        }
                    }
                }
                //序列化Id特殊处理  虽然是常量 但是却是小写
                if(!conventional && !name.equals("serialVersionUID")){
                    messager.printMessage(Kind.ERROR, "静态变量或者常量只能由大写字母与下划线组成,并且以字母开头",e);
                }
            }
    
            //判断变量是否是常量
            private boolean heuristicallyConstant(VariableElement e) {
                if(e.getEnclosingElement().getKind()==INTERFACE) {
                    return true;
                }else if(e.getKind()==FIELD&&e.getModifiers().containsAll(EnumSet.of(STATIC))){
                    return true;
                }else {
                    return false;
                }
    
            }
        }
    }
    

    2.如何运行插入式注解处理器?

    下面是一个简单的运行方法,就是对编译目录下的字节码文件进行校验,通过获取的回调来判断是否出现异常,若异常的话直接抛出即可。
    javac -proc:only -processor 注解处理器的位置 -d 指定放置生成的类文件的位置 处理的java文件位置,可以看看javac的用法

    用法: javac <options> <source files>
    其中, 可能的选项包括:
      -g                         生成所有调试信息
      -g:none                    不生成任何调试信息
      -g:{lines,vars,source}     只生成某些调试信息
      -nowarn                    不生成任何警告
      -verbose                   输出有关编译器正在执行的操作的消息
      -deprecation               输出使用已过时的 API 的源位置
      -classpath <路径>            指定查找用户类文件和注释处理程序的位置
      -cp <路径>                   指定查找用户类文件和注释处理程序的位置
      -sourcepath <路径>           指定查找输入源文件的位置
      -bootclasspath <路径>        覆盖引导类文件的位置
      -extdirs <目录>              覆盖所安装扩展的位置
      -endorseddirs <目录>         覆盖签名的标准路径的位置
      -proc:{none,only}          控制是否执行注释处理和/或编译。
      -processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
      -processorpath <路径>        指定查找注释处理程序的位置
      -parameters                生成元数据以用于方法参数的反射
      -d <目录>                    指定放置生成的类文件的位置
      -s <目录>                    指定放置生成的源文件的位置
      -h <目录>                    指定放置生成的本机标头文件的位置
      -implicit:{none,class}     指定是否为隐式引用文件生成类文件
      -encoding <编码>             指定源文件使用的字符编码
      -source <发行版>              提供与指定发行版的源兼容性
      -target <发行版>              生成特定 VM 版本的类文件
      -profile <配置文件>            请确保使用的 API 在指定的配置文件中可用
      -version                   版本信息
      -help                      输出标准选项的提要
      -A关键字[=值]                  传递给注释处理程序的选项
      -X                         输出非标准选项的提要
      -J<标记>                     直接将 <标记> 传递给运行时系统
      -Werror                    出现警告时终止编译
      @<文件名>                     从文件读取选项和文件名
    
    

    这里可能执行会比较久,为了让校验快一些,所以使用了并行流。

                public void check(Collection<String> javaFilePaths) {
                    String userDir = System.getProperty("user.dir");
                    javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
                    String classFilePath = userDir + File.separator + "target";
                    AtomicInteger errorFileNum = new AtomicInteger(Constants.ZERO);
                    javaFilePaths.parallelStream().forEach(path -> {
                        int result = compiler.run(null, null, null, new String[] { "-proc:only", "-processor",
                                NameCheckProcessor.class.getName(), "-d", classFilePath, path });
                        if (result != Constants.ZERO) {
                            errorFileNum.incrementAndGet();
                        }
                    });
                    if (errorFileNum.get() != Constants.ZERO) {
                        //这个是运行时异常,不过是自己定义的,大家可以直接抛jdk自带的异常
                        throw new ErrorMsg(ResponseMsg.CODE_STYLE_ERROR);
                    }
                }
    

    将校验的代码通过责任链模式来管理(将校验代码当成组件来管理,方便拆卸和组装)

    1.定义一个接口用于约束代码校验的业务方法,也方便拓展

        public interface ValidateCheckService {
            public void check(Collection<String> javaFilePath);//具体的检查逻辑,入参为被检查的java文件路径
            public int order();//用于控制代码校验器的排序
        }
    

    2.对代码校验器进行排序,通过后置处理器即可。

    @Component
    public class ValidateServiceBeanPostProcessor implements BeanPostProcessor{
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if(bean instanceof ValidateCheckService) {
                ValidateConfiguration.getCheckService().add((ValidateCheckService)bean);
                ValidateConfiguration.getCheckService().sort((a, b) -> a.order() - b.order());
            }
            return bean;
        }
    }
    

    3.整合代码校验的代码。

    @Configuration
    public class ValidateConfiguration {
        private static final List<ValidateCheckService> VALIDATE_CODE_SERVICES = new ArrayList<ValidateCheckService>();
        public static List<ValidateCheckService> getCheckService() {
            return VALIDATE_CODE_SERVICES;
        }
    
        private enum CHECK_TYPE {
            CODE_STYLE(0, "codeStyle");
    
            private Integer type;
            private String desc;
    
            public Integer getType() {
                return type;
            }
    
            private CHECK_TYPE(Integer type, String desc) {
                this.type = type;
                this.desc = desc;
            }
        }
    
        public interface ValidateCheckService {
            public void check(Collection<String> javaFilePath);
            public int order();
        }
        
        @Bean
        public ValidateCheckService checkCodeStyle() {
            return new ValidateCheckService() {
                @Override
                public void check(Collection<String> javaFilePaths) {
                    String userDir = System.getProperty("user.dir");
                    javax.tools.JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
                    String classFilePath = userDir + File.separator + "target";
                    AtomicInteger errorFileNum = new AtomicInteger(Constants.ZERO);
                    javaFilePaths.parallelStream().forEach(path -> {
                        int result = compiler.run(null, null, null, new String[] { "-proc:only", "-processor",
                                NameCheckProcessor.class.getName(), "-d", classFilePath, path });
                        if (result != Constants.ZERO) {
                            errorFileNum.incrementAndGet();
                        }
                    });
                    if (errorFileNum.get() != Constants.ZERO) {
                        throw new ErrorMsg(ResponseMsg.CODE_STYLE_ERROR);
                    }
                }
    
                @Override
                public int order() {
                    return CHECK_TYPE.CODE_STYLE.getType();
                }
            };
        }
    }
    
    

    最后整合一下

    @Component
    @Slf4j
    @DependsOn(value = "applicationContextUtils")
    public class CustomApplicationContextEventListener implements ApplicationListener<ContextRefreshedEvent> {
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
                   // 某些类在注入进行实例化的过程中也会创建容器,并且进行刷新
            // 所以只有这样子判断,当AnnotationConfigServletWebServerApplicationContext刷新完,整个容器的刷新过程才结束
            boolean validateContextType = event
                    .getApplicationContext() instanceof AnnotationConfigServletWebServerApplicationContext;
            if (!validateContextType) {
                return;
            }
            // 生产环境的时候没必要进行校验.
            String active = ApplicationContextUtils.getActiveProfile();
            log.info("CustomApplicationContextEventListener onApplicationEvent active = " + active);
            if (Constants.PROD.equals(active)) {
                return;
            }
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            List<String> javaFilePaths = new ArrayList<String>();
            String userDir = System.getProperty("user.dir");
            getResource(userDir, javaFilePaths);
            //对所有的java文件进行检查
            ValidateConfiguration.getCheckService().forEach(service -> {
                service.check(javaFilePaths);
            });
            stopWatch.stop();
            log.info(String.format("校验代码格式耗费时间为%d毫秒", stopWatch.getTime()));
        }
    
        //获取所有的java文件路径
        private void getResource(String path, List<String> javaFilePaths) {
            // 编译结果跟测试类均跳过
            if (path.contains(File.separator + "target" + File.separator)
                    || path.contains(File.separator + "test" + File.separator)) {
                return;
            }
            File file = new File(path);
            if (!file.isDirectory() && file.getName().endsWith(".java")) {
                javaFilePaths.add(file.getPath());
                return;
            }
            if (file.listFiles() == null) {
                return;
            }
            List<File> fileLists = Arrays.asList(file.listFiles());
            fileLists.forEach(fileItem -> {
                getResource(fileItem.getAbsolutePath(), javaFilePaths);
            });
        }
    }
    

    运行结果

    最后的运行结果,校验代码有点粗糙,尚未调整,不过能跑起来先算完成一大步了。


    运行结果

    相关文章

      网友评论

          本文标题:制作一个简单的代码校验器

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