美文网首页
SpringBoot相关

SpringBoot相关

作者: 神马_01 | 来源:发表于2019-05-20 09:47 被阅读0次

组件适配

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) {
        ...
    }
}

相关文章

网友评论

      本文标题:SpringBoot相关

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