组件适配
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的实现。实现如下:
- 实现接口PropertySourceLocator,对统一配置进行相应解析
public class ScmConfigPropertySourceLocator implements PropertySourceLocator {
@Override
public PropertySource<?> locate(Environment environment) {
...
}
}
- 让ScmConfigPropertySourceLocator生效
@Configuration
@AutoConfigureBefore(PropertySourceBootstrapConfiguration.class)
public class ScmConfigServerAutoConfiguration {
@Bean
public ScmConfigPropertySourceLocator scmConfigPropertySourceLocator() {
...
}
}
- 最后需要在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);
...
}
}
其中typeHandlerClass和LocalDateTimeTypeHandler的实现方式类似。
mybatis-generator
- 自动生成代码需要去扩展mybatis-generator-core,要做的事情主要处理三个方面:DAO接口、model领域模型、生成的XML文件。
- DAO接口通过继承AbstractJavaClientGenerator,其实生成的接口类比较简单,主要是需要引用类方面需要注意。
- model领域模型通过继承AbstractJavaGenerator进行相应的扩展,由于我们使用了lombok,相应的set和get方法不需要去考虑,只要把数据库字段转换成相应的字段即可。在Boolean和枚举类型中,是根据表中的注释生成的,会定义一些固定格式,通过正则表达式进行相应的匹配,然后生成相应的领域模型。
- XML文件通过继承AbstractXmlGenerator来进行扩展,会生成相应的模板SQL语句,我们处理了include相关的内容,在以后新增的表字段,尽量少出错,不要手写单表字段。
减少代码
关于减少代码方面,我们希望的是只关心业务代码,另外代码的可读性尽量的高(joke:代码行越少,说明单行价格越高,所以我们要写贵的代码),方法代码行数多了,愿意读的人就很少了。更希望代码方法能够描述业务逻辑,如果超过了5个单词,该方法就要好好想想是不是需要对代码进行拆分。
类型转换
- 类型的代码其实是有固定的套路,但是没有必要手写转换成字符串(比如:LocalDateTime,有些会定义两个对象,时间对象,时间字符串对象):
- @RequestBody:继承com.fasterxml.jackson.databind.JsonDeserializer
- @ResponseBody:继承com.fasterxml.jackson.databind.JsonSerializer
- 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) {
...
}
}