在做技术选型的时候一直存在着两个声音,mongo作为数据库比较mysql好,mysql做为该数据比mongo好。当然不同数据库都有有着自己的优势,我们在做技术选型的时候无非就是做到对数据库的扬长避短。
mysql最大的优势就是支持事务,事务的五大特性保证的业务可靠性,随之而来的就是事务会产生的问题:脏读、幻读、不可重复度,当然我们也会使用不同的隔离级别来解决。(最典型的业务问题:银行存取钱)
mongo无非就是将所有的关联数据存储到同一个集合中,虽然存储简单,但是数据的原子性和一致性我们又该如何保证呢?
(小孩子才做选择,我全都要!)
我们的项目中会同时拥有两套代码,一套是基于mysql,一套是基于mongo。当然不排除其他数据库,我们也做到了已扩展。
导致的配置切换流程就是:在apollo中设置对应的配置(比如:v1,v2),在代码中就会做切换,切我们无需重启项目就可以做到,是不是很酷。
1.编写bean后缀命名配置类
mysql的beanName配置类
//自定义BeanNameGenerator,自定义bean的Name,通过实现AnnotationBeanNameGenerator类并重写PrefixBeanNameGenerator方法
//我们通过自定义bean的名字来控制bean为不同的版本
@Configuration
@ComponentScan(value = {"com.fjhb.ms.scheme.learning.query"},
//过滤掉被SpringBootApplication注解修饰的类(不操作启动类)
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = SpringBootApplication.class)},
nameGenerator = MSSchemeLearningAutoConfiguration.PrefixBeanNameGenerator.class)
public class MSSchemeLearningAutoConfiguration {
public static class PrefixBeanNameGenerator extends AnnotationBeanNameGenerator {
@Override
protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize("ms-scheme-learning-query-front-gateway" + "-" + shortClassName);
}
}
}
mongo的beanName配置类(在beanName的后缀添加-V1标识)
@Configuration
@ComponentScan(value = {"com.fjhb.ms.scheme.learning.queryV1"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = SpringBootApplication.class)},
nameGenerator = MSSchemeLearningAutoConfigurationV1.PrefixBeanNameGenerator.class)
public class MSSchemeLearningAutoConfigurationV1 {
public static class PrefixBeanNameGenerator extends AnnotationBeanNameGenerator {
@Override
protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize("ms-scheme-learning-query-front-gateway" + "-" + shortClassName + "-v1");
}
}
}
2.编写自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface GraphQLProxyBean {
//主要的作用就是保证vlaue和proxyBeanName可以相互读取
//为value属性设置新的名字 proxyBeanName
@AliasFor("proxyBeanName")
String value() default "";
//为属性设置新的名字 value
@AliasFor("value")
String proxyBeanName() default "";
//设置BeanName的前缀
String prefix() default "";
//设置BeanName的后缀
String suffix() default "";
/**
* 代理查询版本
*/
QueryVersion proxyQueryVersion();
}
该自定义注解主要的作用就是传递我们自定义BeanName的属性,就比如:前缀。使用的位置:在需要区分版本的Gql方法上,也就是对应的接口上。如果没有这个自定义直接修饰的话,此Gql方法就会变成一个通用方法。
3.编写aop自定义切面类
//代码分析未完成
@Aspect
@Component
//继承BeanFactoryAware的目的主要就是为了拿得到BeanFactory类
public class GraphQLProxyAspect implements BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
@Autowired
private QueryVersionSelector queryVersionSelector;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
//ConfigurableListableBeanFactory 用来对bean定义管理,自动装配的一系列的操作
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
}
//匹配所有被GraphQLProxyBean注解修饰的方法,后面的excution还匹配了所有的公共方法,并排除了getSchemaName方法
@Around("@within(com.fjhb.data.query.utils.annotations.GraphQLProxyBean) && execution(public * *(..)) && !execution(* getSchemaName(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//方法不存在注解放行
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
if (method == null) {
return joinPoint.proceed();
}
Class>[] parameterTypes = method.getParameterTypes();
//判断判断有没有被GraphQLProxyBean修饰,没有直接放行
GraphQLProxyBean proxyBeanAnnotation = AnnotationUtils.findAnnotation(joinPoint.getTarget().getClass(), GraphQLProxyBean.class);
if (proxyBeanAnnotation == null) {
return joinPoint.proceed();
}
//********************** 重点位置***************************** //读取我们在指定的代理类型,这里可能是V1也就是mongo版本,也可能是V2就是mysql版本 QueryVersion queryVersion = proxyBeanAnnotation.proxyQueryVersion();
if (queryVersion == null) {
throw new IllegalArgumentException("GraphQL Proxy Exception: param[QueryVersion] of @GraphQLProxyBean is empty");
}
//判断是否走代理
Object target = joinPoint.getTarget();
//获取类的最终实现类
Class> targetClass = AopProxyUtils.ultimateTargetClass(target);
//获取getSchemaName方法
Method getSchemaNameMethod = ReflectionUtils.findMethod(targetClass, "getSchemaName");
//********************会到apollo上获取对应的版本号 (因为此时我们就两个version,只要版本号不一致那就什么是另一种版本,就会直接使用当前方法,就比如:此时直接上设置的是v1-mysql,但是在apollo上设置的是v2-mongo版本,此时就会直接执行当前方法,不会去切换成mysql版本,直接执行默认的方法,默认方法是mongo版本的,所以我们是以apollo上的配置为准)
//根据平台版本号和gqlSchemeName获取查询版本号
//***********************自定义方法从apollo中获取版本号gqlQueryVersion
//判断手动设置的版本号和数据库中存储的版本号是否一致,不一致的话就直接放行queryVersion(从注解类中获取设置的版本号)
if (gqlQueryVersion != queryVersion) {
return joinPoint.proceed();
}
String customProxyBeanName = proxyBeanAnnotation.proxyBeanName();
//拿到最终的实现类名
String originalBeanName = getOriginalBeanName(joinPoint.getTarget());
//****************************重点代码*********************
//这里就是切面的主要目的,将我们原有的bean的名字加上对应的前后缀组成我们之前值配置类中给不同版本类加上的名字
String proxyBeanName;
//customProxyBeanName不为空
if (StringUtils.hasText(customProxyBeanName)) {
// 指定了代理 Bean 名称,直接使用该名称
proxyBeanName = customProxyBeanName;
} else {
//如果没有指定代理代理Bean的名字的话,就会将我们切面类的类名加上前后缀,作为代理Bean的名字
String prefix = proxyBeanAnnotation.prefix();
String suffix = proxyBeanAnnotation.suffix();
proxyBeanName = buildProxyBeanName(originalBeanName, prefix, suffix);
}
if (beanFactory.containsBean(proxyBeanName)) {
//获取对应的bean实例
Object proxyBean = beanFactory.getBean(proxyBeanName);
//对应的调用方法
Method proxyMethod = ReflectionUtils.findMethod(proxyBean.getClass(), method.getName(), parameterTypes);
if (proxyMethod != null) {
//将对应的方法设置为Accessible,因为大多的方法的作用域是private
ReflectionUtils.makeAccessible(proxyMethod);
try {
//最终执行方法
return proxyMethod.invoke(proxyBean, joinPoint.getArgs());
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
} else {
throw new NoSuchBeanDefinitionException("GraphQL Proxy Exception: gql proxy bean not found[" + proxyBeanName + "]");
}
// 如果找不到代理 bean 或方法,调用原来的方法
return joinPoint.proceed();
}
//拼接前后缀方法
private String buildProxyBeanName(String originalBeanName, String prefix, String suffix) {
String newBeanName = originalBeanName;
if (StringUtils.hasText(prefix)) {
newBeanName = prefix + newBeanName;
}
if (StringUtils.hasText(suffix)) {
newBeanName = newBeanName + suffix;
}
return newBeanName;
}
//获取类的最终实现类
private String getOriginalBeanName(Object original) {
Class> originalClass = AopProxyUtils.ultimateTargetClass(original);
String[] beanNames = beanFactory.getBeanNamesForType(originalClass);
for (String beanName : beanNames) {
Object beanInstance = beanFactory.getBean(beanName);
if (isSameInstance(beanInstance, original)) {
return beanName;
}
}
throw new NoSuchBeanDefinitionException("GraphQL Proxy Exception: Bean name not found for original class[" + originalClass.getName() + "]");
}
//判断是否两个类是否实现了相同的接口
private boolean isSameInstance(Object beanInstance, Object original) {
Object ultimateOriginal = AopProxyUtils.ultimateTargetClass(original);
Object ultimateBeanInstance = AopProxyUtils.ultimateTargetClass(beanInstance);
return ultimateOriginal.equals(ultimateBeanInstance) || ObjectUtils.nullSafeEquals(beanInstance, original);
}
}
4.配置apollo
其实这个切换模式准确的来讲用到了:AOP + 反射 来实现的,因为目前就考虑两种数据库的方式,如果未来使用了其他的数据库,那基本上代码的实现思路基本没有变,最终我们只需要在apollo上切换设置就可以动态的实现不同版本的方法的切换。
也主要是为了后续的高拓展性,后续如果想要做混合使用或者使用其他的数据库,在策略配置类和apollo配置即可实现动态切换不同的方法版本。