由于在公司的时候,发现口头上的代码规范,没有一个人遵守,于是乎。想着做一个什么东西来强制大家代码上的使用。
我的想法很简单粗暴,规范有问题,容器无法启动,无法进行单元测试。这样子一来,只要有人没有按规范来,目的就达到了。
考虑到的点如下。
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);
});
}
}
运行结果
最后的运行结果,校验代码有点粗糙,尚未调整,不过能跑起来先算完成一大步了。
运行结果
网友评论