SpringBoot相关

组件适配

jboss

  • 为了能够支持容器部署,故增加了如下内容,主要能够起到替代web.xml的作用
public class ServletInitializer extends SpringBootServletInitializer {

    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(DemoBizApplication.class);
    }
}
  • 新增目录src/main/webapp/WEB-INF,并在该目录中增加jboss-deployment-structure.xml文件,进行相应的包排除。

数据源

  • 由于数据源使用的JNDI,故不使用application.properties中配置的数据源,所以在main函数的注解中增加排除数据源配置,代码如下
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
  • 需要根据不同的环境激活不同的bean,在不同的环境中需要激活不同bean
    @Bean
    @Profile({"dev"})
    public ServletWebServerFactory servletContainer() {
        return new TomcatServletWebServerFactory() {
            @Override
            protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
                tomcat.enableNaming();
                return super.getTomcatWebServer(tomcat);
            }

            @Override
            protected void postProcessContext(Context context) {
                ContextResource resourceWrite = new ContextResource();
                resourceWrite...
                context.getNamingResources().addResource(resourceWrite);

                super.postProcessContext(context);
            }
        };
    }

    @Bean
    @Profile({"dev"})
    public DataSource dataSource() throws IllegalArgumentException, NamingException {
        JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
        bean.setJndiName("JNDI_NAME");
        bean.setProxyInterface(DataSource.class);
        bean.setLookupOnStartup(true);
        bean.afterPropertiesSet();
        return (DataSource) bean.getObject();
    }

    @Bean
    @Profile({"prod", "sit"})
    public DataSource prodDataSource() throws IllegalArgumentException, NamingException {
        JndiObjectFactoryBean bean = new JndiObjectFactoryBean();
        bean.setJndiName("JNDI_NAME");
        bean.setProxyInterface(DataSource.class);
        bean.setLookupOnStartup(true);
        bean.afterPropertiesSet();
        return (DataSource) bean.getObject();
    }

统一资源配置

目前公司有统一的资源配置相关内容,我们需要做的是能够在启动时能够替换application.properties中的内容,所以我们参考了spring-cloud的实现。实现如下:

  1. 实现接口PropertySourceLocator,对统一配置进行相应解析
public class ScmConfigPropertySourceLocator implements PropertySourceLocator {
    
    @Override
    public PropertySource<?> locate(Environment environment) {
        ...
    }
}
  1. 让ScmConfigPropertySourceLocator生效
@Configuration
@AutoConfigureBefore(PropertySourceBootstrapConfiguration.class)
public class ScmConfigServerAutoConfiguration {
   
    @Bean
    public ScmConfigPropertySourceLocator scmConfigPropertySourceLocator() {
        ...
    }
}
  1. 最后需要在META-INFO/spring.factories定义相应的BootstrapConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
ScmConfigServerAutoConfiguration

这样设置后,我们就可以使用统一配置来进行参数、资源的配置,如果读取不到统一配置,则会以本地文件为准。

starter与动态注册

Starter主要分为两部分,一部分是依赖的类,另一部分是引入依赖pom文件:在starter实现类中的pom文件中scope一般是provided;在具体的starter的pom文件中具体引用相应的包。
其中我们通过下面的套路来实现Starter的动态注册

ImportBeanDefinitionRegistrar

  • 首先增加注解,此注解在main函数中引用,就激活了实现类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({XXXConfigBindingRegistrar.class})
@Documented
public @interface EnableXXX {

    String configFileName() ;

    Class<? extends AbstractClient> type() ;
}
  • 在注解中引入相应的ImportBeanDefinitionRegistrar
public class XXXConfigBindingRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(EnableXXX.class.getName()));

        assert attributes != null;

        String configFileName = attributes.getString("configFileName");
        Class<?> type = attributes.getClass("type");

        AbstractBeanDefinition definition = BeanDefinitionBuilder.rootBeanDefinition(type).addConstructorArgValue(configFileName).getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition(Objects.requireNonNull(definition.getBeanClassName()), definition);
    }
}

ImportSelector

此种套路和ImportBeanDefinitionRegistrar基本一致,也是需要进行如下两步:

  • 增加相应注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(XXXConfigurationSelector.class)
public @interface EnableXXX {
    XXXMode value();
}
  • 在注解中导入相应的ImportSelector,在此过程中会注册相应的类至容器中
public class XXXConfigurationSelector implements ImportSelector {
   
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        XXXMode mode = (XXXMode) metadata.getAnnotationAttributes(EnableXXX.class.getName()).get("value");
        if (mode == ...) {
            return new String[]{xxx.class.getName(), xxx.class.getName()};
        }
        if (mode == ...) {
            return new String[]{xxx.class.getName()};
        }
        return new String[]{xxx.class.getName()};
    }


}

其他@Import方式

@Import还可以导入@Configuration,不过一般扫描路径和配置starter的路径不一样,代码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableCaching
@Import(XXXConfiguration.class)
public @interface EnableXXX {
    String applicationName();
}

定义相应的初始化类,另外通过ImportAware读取相应的注解内容

@Configuration
public class SpringCacheConfiguration implements ImportAware, ApplicationContextAware {

    private AnnotationAttributes enableXXX;

    private ApplicationContext applicationContext;

    @Bean(name = "xxx")
    public XXX xxx() {
        ...
    }


    @Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        this.enableXXX = AnnotationAttributes.fromMap(importMetadata.getAnnotationAttributes(EnableXXX.class.getName(), false));
        if (this.enableXXX == null) {
            throw new IllegalArgumentException(
                    "@EnableXXX is not present on importing class " + importMetadata.getClassName());
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

ConfigurationProperties配合EnableConfigurationProperties

此种套路是通过application中配置相应的参数进行启用,配置参数较多是,此种方式比较合适

  • ConfigurationProperties读取application.properties中的配置参数,定义如下:
@Data
@ConfigurationProperties(prefix = "snf.boot.uaa")
public class UaaProperties {

    private boolean enabled = false;

    private String uaaSystemCode;

    private String uaaServiceURL;

    private String[] urlPatterns;   
}
  • EnableConfigurationProperties读取相关bean,另外激活和配置相应的bean
@Configuration
@ConditionalOnProperty(prefix = "snf.boot.uaa", name = "enabled")
@ConditionalOnClass(UaaProperties.class)
@EnableConfigurationProperties(UaaProperties.class)
public class UaaAutoConfiguration {

    private UaaProperties uaaProperties;

    public UaaAutoConfiguration(UaaProperties uaaProperties) {
        this.uaaProperties = uaaProperties;
    }

    @Bean
    public XXX xxx() {
       ...
    }

}

脚本管理、Domain、DAO

脚本的管理一直是开发中的痛点,SQL脚本理论上应该是单元测试的一部分,如果用其他可视化工具进行维护,没有相应的版本的概念,另外在初始化项目时,可以快速的进行初始化。

Flyway

目前Flyway只有在单元测试中启用,其他环境是不启用相应的脚本的。由于使用的JNDI,而自动化测试用例中并不会启用容器,索引在测试的文件中增加相应的数据源配置,另外TestNG中使用Flyway进行如下配置:

@ActiveProfiles(value = "autotest")
public class DemoBizApplicationTests {

    public static void main(String[] args) {
        SpringApplication.run(DemoBizApplicationTests.class, args);
    }

    @Bean(name = "dataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(initMethod = "migrate")
    public Flyway createFlyway(DataSource dataSource) {
        return Flyway.configure().dataSource(dataSource).baselineVersion("0").baselineOnMigrate(true).load();
    }

}

mybatis-spring和mybatis-generator

目前自动生成插件,对枚举(领域模型中是枚举,数据库中是整数值)、Boolean类型的支持并不是非常好,所以对mybatis-generator和mybatis-spring进行相应的改造。其中涉及到mybatis-spring和mybatis-generator相关的改造。

mybatis-spring

枚举值和一般类型的对象处理有很大的区别,其他固定的类型可以(比如LocalDateTime)通过如下方式处理:

@MappedTypes(LocalDateTime.class)
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {
   ...
}

枚举一般是很多类型,这需要我初始化时扫描相应的枚举类型加入到映射中,所以首先我们修改了mybatis-spring中的SqlSessionFactoryBean,增加了空函数

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
  ...
  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
    ...
    this.configBeforeXXX(configuration);
    ...
  }

  protected void configBeforeXXX(Configuration configuration) {
  }
  ...
}

所有注册枚举的方式在子类中实现,通过继承SqlSessionFactoryBean,在configBeforeXXX中进行初始化类,由于同一的类处理,枚举实现统一的接口

public interface DBEnum {
    int getIntValue();
}

最后通过如下方式注册

public class SportsSqlSessionFactory extends SqlSessionFactoryBean {
    ...
    @Override
    protected void configBeforeXXX(Configuration config) {
        
        TypeHandlerRegistry typeHandlerRegistry = config.getTypeHandlerRegistry();
        ...
         typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
        ...
    }
}

其中typeHandlerClassLocalDateTimeTypeHandler的实现方式类似。

mybatis-generator

  • 自动生成代码需要去扩展mybatis-generator-core,要做的事情主要处理三个方面:DAO接口、model领域模型、生成的XML文件。
  1. DAO接口通过继承AbstractJavaClientGenerator,其实生成的接口类比较简单,主要是需要引用类方面需要注意。
  2. model领域模型通过继承AbstractJavaGenerator进行相应的扩展,由于我们使用了lombok,相应的set和get方法不需要去考虑,只要把数据库字段转换成相应的字段即可。在Boolean和枚举类型中,是根据表中的注释生成的,会定义一些固定格式,通过正则表达式进行相应的匹配,然后生成相应的领域模型。
  3. XML文件通过继承AbstractXmlGenerator来进行扩展,会生成相应的模板SQL语句,我们处理了include相关的内容,在以后新增的表字段,尽量少出错,不要手写单表字段。

减少代码

关于减少代码方面,我们希望的是只关心业务代码,另外代码的可读性尽量的高(joke:代码行越少,说明单行价格越高,所以我们要写贵的代码),方法代码行数多了,愿意读的人就很少了。更希望代码方法能够描述业务逻辑,如果超过了5个单词,该方法就要好好想想是不是需要对代码进行拆分。

类型转换

  • 类型的代码其实是有固定的套路,但是没有必要手写转换成字符串(比如:LocalDateTime,有些会定义两个对象,时间对象,时间字符串对象):
  1. @RequestBody:继承com.fasterxml.jackson.databind.JsonDeserializer
  2. @ResponseBody:继承com.fasterxml.jackson.databind.JsonSerializer
  3. form表单/get请求:实现org.springframework.format.Formatter.Formatter被处理的对象必须有无参构造函数,如果没有,用组合方式把该对象作为另外一个对象的成员变量

统一异常

统一异常的主要目的是业务代码上能够尽量少处理异常,少一些无用的模板代码,统一异常其实也是相关套路,处理方式如下:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object exceptionHandler(Exception ex) {
        ...
    }
}

参数校验

  • 参数校验必不可少,但是在处理参数的时候,大多数在Controller中进行参数判断,一个Controller中60%的都是参数校验代码,这部分代码大部分是简单的判断,会夹杂着业务代码,影响可维护性。
  • 我们使用了注解@Validated,其中比较好的是新增的group,同一个对象这样就可以在不同的业务场景复用了,只要参数校验分组就可以了。
  • 对于自定义对象校验,可以使用如下套路:

定义相应的注解

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {XXXGeneratorValidator.class})
public @interface XXXGenerator {

    String message() default "";

    int length();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        XXXGenerator[] value();
    }
}

定义相应的处理类

public class XXXGeneratorValidator implements ConstraintValidator<XXXGenerator, String> {

    private int length;

    @Override
    public void initialize(XXXGenerator generator) {
        this.length = generator.length();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        ...
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容