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

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

作者: 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);
        });
    }
}

运行结果

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


运行结果

相关文章

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

    由于在公司的时候,发现口头上的代码规范,没有一个人遵守,于是乎。想着做一个什么东西来强制大家代码上的使用。我的想法...

  • 校验器

    数据通过校验器校验,如果校验不通过,拦截器抛出错误,校验器下面的代码就不能继续执行。 拓展:校验器负责数据校验,拦...

  • 数据校验器架构模式组

    问题引出 隔离校验器 代码如下: 清单 1: UserInfoValidator.java 可组装校验器 清单 2...

  • 微信授权

    1、配置微信公众平台服务器配置 校验模块代码: SignUtil (校验帮助类) WeiXinConterolle...

  • Angular2+ 表单校验器的使用

    校验器逻辑定义: 校验器的使用: LIST 校验器: 其他校验器 HTML 中的使用 样式:

  • Swift 相册、相机、麦克风、定位权限判断

    1、相册权限校验,代码如下: 2、相机权限校验,代码如下: 3、麦克风权限校验,代码如下: 4、定位权限校验,代码...

  • iOS:CocoaPods制作私有库

    本章制作私有库方法省去了繁琐的Pod校验,快速简单制作私有库。 创建索引库 我使用的是GitHub 制作Pod 再...

  • (九)2表单验证

    (一)响应式表单校验最简单的控制器校验器语法。只能返回对象或null(null代表验证通过),返回值的key只能是...

  • Swift之一个简单的加法计算器

    写一个简单的加法计算器,代码如下: 几行简单的代码。

  • GD库——校验码简单封装函数

    前言:下面给出校验码封装函数代码,刚学代码比较简单;具体看gitHub

网友评论

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

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