栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

手撸一个动态数据源的Starter 完整编写一个Starter及融合项目的过程 保姆级教程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

手撸一个动态数据源的Starter 完整编写一个Starter及融合项目的过程 保姆级教程

手撸一个动态数据源的Starter!

文章目录
  • 手撸一个动态数据源的Starter!
  • 前言
  • 一、准备工作
    • 1,项目目录结构
    • 2,POM文件
  • 二、思路
  • 三、编写代码
    • 1,定义核心注解 Ds
    • 2,定义切面
    • 3,定义DataSourceContextHolder
    • 4,定义抽象动态数据源模板类及其实现子类
    • 5,定义application.yml配置类
    • 6,定义AutoConfiguration
    • 6,定义DataSourceCreator创建器
  • 四 实测准备
    • 1,数据库准备
    • 2,表准备
    • 3,项目准备
  • 五 实测
  • 总结


前言

该项目借鉴于苞米豆的开源项目dynamic-datasource-spring-boot-starter
码云地址
本项目地址:
码云地址

你是否已经做烦了公司里的CRUD。想要编写一些不一样的代码,比如编写一些SpringBoot中间件,还可以融入公司项目中,让别人也使用你自己的Starter,这样是不是突出了你的重要性(我不承认我是为了装逼,手动狗头) 直接进入正题。
涉及知识点:
  • SpringBoot自动装配
  • ThreadLocal的使用
  • ArrayDeque 后进先出栈的使用
  • Aop相关知识点

提示:以下是本篇文章正文内容

一、准备工作 1,项目目录结构

2,POM文件


    4.0.0

    com.xzq
    dynamic-spring-boot-starter
    1.0-SNAPSHOT

    
        8
        8
        1.2.8
        3.4.3
    

    
            org.springframework.boot
            spring-boot-starter-parent
            2.5.6
    

    
        
            com.alibaba
            druid-spring-boot-starter
            ${druid.version}
        
        
            org.springframework.boot
            spring-boot-starter
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
            com.baomidou
            mybatis-plus-boot-starter
            ${mybatis.plus.version}
        
        
            org.springframework.boot
            spring-boot-configuration-processor
        
        
            org.projectlombok
            lombok
            true
        
    
    
        

            
                org.apache.maven.plugins
                maven-source-plugin
                2.1.2
                
                    
                        attach-sources
                        
                            jar
                        
                    
                
            
        
    

二、思路


整体的过程如上图所示。

  1. 我们通过核心注解Ds,将数据库路由Key标注Controller 或者Service或者Mapper的类或者方法之上。
  2. 然后通过Aop横切技术,在执行方法时进行拦截,获取到路由Key设置到ThreadLocal中。
  3. 最后由持久层获取Connection时,动态数据源会从ThreadLocal中获取路由Key,然后根据路由Key找到对应的数据源,由该数据源创建Connection连接。
  4. 通过Connection连接操作数据库

我们仔细思考会不会发现一些问题呢?
如果说是通过ThreadLocal存储当前的数据库路由Key,那么ThreadLocal中应该存储什么类型的数据呢?直接String?
直接String的话,当一个Service标注了Ds,假如我又调用了另外一个Service,另外的一个Service中也标注了Ds,既两个不同的Service是不同库的,如果ThreadLocal使用String类型的数据,我们使用Aop横切技术,第一个Service会被拦截,将路由Key设置到本地线程变量中,第二个Service执行,又被拦截又将当前路由Key设置到ThreadLocal中,或许有人问,这有什么问题?,那么我第一个Service调用完第二个Service后,数据库路由发生了改变,可是该Service中的方法还没有执行完毕,我后续的一些数据库操作实际上连接的都是第二个Service标注的数据源。这肯定会出问题的。这里先留下伏笔,小伙伴们可以思考一些使用什么样的数据结构可以避免。

三、编写代码 1,定义核心注解 Ds

com.xzq.dynamic.annotation.Ds

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Inherited
public @interface Ds {

    String value() default "";

}

首先第一步,我们先自定义核心注解Ds,其中的value表示路由数据库的Key
通过元注解@Target的ElementType属性表示,该注解可作用在方法或者类,接口之上。

2,定义切面
package com.xzq.dynamic.aop;
@Aspect
public class DsAspect {

    private Logger logger = LoggerFactory.getLogger(DsAspect.class);

    @Pointcut("@annotation(com.xzq.dynamic.annotation.Ds)")
    public void aopPoint() {
    }

    @Around("aopPoint()")
    public Object dbRouter(ProceedingJoinPoint pjp) throws Throwable {
        Ds ds = findDsKey(pjp);
        if (ds == null) {
            return pjp.proceed();
        }
        String value = ds.value();
        if (StringUtils.hasLength(value)) {
            logger.info("拦截目标方法{},设置数据库路由Key: {}", ((MethodSignature) pjp.getSignature()).getMethod().getName(), value);
            DataSourceContextHolder.push(value);
        }
        try {
            return pjp.proceed();
        } finally {
            DataSourceContextHolder.poll();
        }
    }

    private Ds findDsKey(ProceedingJoinPoint pjp) {
        //获取目标对象
        Class target = pjp.getTarget().getClass();
        //获取目标对象所有接口
        Class[] interfaces = target.getInterfaces();
        //获取方法对象
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        //Method优先级 > 类优先级 > 接口优先级
        Ds ds = null;
        if ((ds = getDs(method)) == null &&
                (ds = getDs(new Class[]{target})) == null &&
                (ds = getDs(interfaces)) == null) {
            return null;
        }
        return ds;
    }

    private Ds getDs(Class[] targets) {
        for (Class target : targets) {
            Ds ds = target.getAnnotation(Ds.class);
            if (ds != null) {
                return ds;
            }
        }
        return null;
    }

    private Ds getDs(Method method) {
        return method.getAnnotation(Ds.class);
    }
}


该切面的核心方法其实就是findDsKey(), 在该方法中获取切点的目标对象,方法对象,还有目标对象的实现接口。然后设置一个获取的优先级,方法上的Ds优先于类,实现类优先于接口。
这样可以避免在类上设置了路由,但是在该类中的某一个方法需要切换数据库也设置了路由,导致类覆盖方法的路由。
为什么还要获取接口的呢?我们一般都会设置到实现类上。但是如果用过Mybatis的小伙伴们肯定知道,Mybatis中并没有实现类,只有接口,实现类是在程序运行中创建的。通过JDK的动态代理。

但是这样写就行了吗?如果有对Aop比较熟悉的小伙伴肯定知道,这样其实是不行的,一开始我也是这样写的,后来测试的时候发现,如果将Ds注解标注在类上,切面并不会拦截该类的所有方法。(哎,还是楼主太菜对Aop基础的匮乏)
后面进行实际测试的时候会介绍如何绕开Aop的方法匹配器。
Spring中文文档

3,定义DataSourceContextHolder
package com.xzq.dynamic.core;

public class DataSourceContextHolder {
    
    private static final ThreadLocal> DB_KEY_HOLDER = new ThreadLocal>() {
        @Override
        protected Deque initialValue() {
            return new ArrayDeque<>();
        }
    };
    
    public static String peek(){
       return DB_KEY_HOLDER.get().peek();
    }

    
    public static void push(String key){
         DB_KEY_HOLDER.get().push(key);
    }

    
    public static void poll() {
        Deque deque = DB_KEY_HOLDER.get();
        deque.poll();
        if (deque.isEmpty()) {
            clean();
        }
    }
    public static void clean() {
        DB_KEY_HOLDER.remove();
    }
}

创建了一个路由Key的持有类,其实也就是利用ThreadLocal来实现线程隔离。
在这里我们使用ArrayDeque当作ThreadLocal的数据类型,这是一个使用LIFO 队列,后进先出,其实也就是我们常说的栈结构。
通过使用这种数据类型,我们就可以实现,各个方法上定义的路由Key互不影响

4,定义抽象动态数据源模板类及其实现子类

动态路由抽象类

package com.xzq.dynamic.core;

@Data
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
	//多数据源容器
    private Map dataSourceMap = new ConcurrentHashMap<>();
    //默认主数据库
    private String primaryKey;

    @Override
    public Connection getConnection() throws SQLException {
      return   determineDataSource().getConnection();
    }
    //抽象模板方法,交由子类实现
    protected abstract DataSource determineDataSource();
    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineDataSource().getConnection(username, password);
    }

    protected DataSource getDataSource(String dbKey) {
        if (!StringUtils.hasLength(dbKey)) {
            return dataSourceMap.get(primaryKey);
        }
        return dataSourceMap.get(dbKey);
    }
}

动态路由实现类

package com.xzq.dynamic.core;

public class DynamicRoutingDataSource extends AbstractRoutingDataSource{


    @Override
    protected DataSource determineDataSource() {
        return getDataSource(DataSourceContextHolder.peek());
    }


}

其实Spring-jdbc中提供了一个抽象动态数据源模板类

有兴趣的小伙伴可以看一下。
我们在这里自己去定义了一个动态路由抽象模板类。该类的作用是什么呢?
首先先解释一下为啥叫他模板类,这里其实是应用了设计模式中的模板模式。由抽象类定义方法的执行顺序,再有子类实现方法。这里就不多说了。
该类实现了AbstractDataSource,重写了getConnection()方法。
我们定义了一个抽象模板方法,determineDataSource() 其作用就是获取真正的数据源,这也是我们动态数据源的核心方法,其DynamicRoutingDataSource 子类重写了该方法,通过ThreadLocal获取到路由key,从dataSourceMap中拿到匹配的数据源。

5,定义application.yml配置类
package com.xzq.dynamic.spring;

@ConfigurationProperties(prefix = DynamicProperties.PREFIX)
@Data
public class DynamicProperties {

    public static final String PREFIX = "spring.datasource.dynamic";
    
    private Map datasource = new linkedHashMap<>();
    
    private DruidConfig druid;

    
    private HikariCpConfig hikari;
    
    private String primary;

    
    private Class type;
}

package com.xzq.dynamic.spring;

import lombok.Data;

import java.util.Properties;

@Data
public class DataSourceProperty {
    
    private String driverClassName;
    
    private String url;
    
    private String username;
    
    private String password;

}
package com.xzq.dynamic.spring.druid;

@Data
public class DruidConfig {
    private Integer initialSize;
    private Integer maxActive;
    private Integer minIdle;
    private Integer maxWait;
    private Long minEvictableIdleTimeMillis;
    private Long maxEvictableIdleTimeMillis;

    String INITIAL_SIZE = "druid.initialSize";
    String MAX_ACTIVE = "druid.maxActive";
    String MIN_IDLE = "druid.minIdle";
    String MAX_WAIT = "druid.maxWait";
    String MIN_EVICTABLE_IDLE_TIME_MILLIS = "druid.minEvictableIdleTimeMillis";
    String MAX_EVICTABLE_IDLE_TIME_MILLIS = "druid.maxEvictableIdleTimeMillis";
    public Properties toPropertes() {
        Properties properties = new Properties();
        properties.setProperty(INITIAL_SIZE, String.valueOf(initialSize));
        properties.setProperty(MAX_ACTIVE, String.valueOf(maxActive));
        properties.setProperty(MIN_IDLE, String.valueOf(minIdle));
        properties.setProperty(MAX_WAIT, String.valueOf(maxWait));
        properties.setProperty(MIN_EVICTABLE_IDLE_TIME_MILLIS, String.valueOf(minEvictableIdleTimeMillis));
        properties.setProperty(MAX_EVICTABLE_IDLE_TIME_MILLIS, String.valueOf(maxEvictableIdleTimeMillis));
        return properties;
    }
}
package com.xzq.dynamic.spring.hikari;
@Data
public class HikariCpConfig {

    private String catalog;
    private Long connectionTimeout;
    private Long validationTimeout;
    private Long idleTimeout;
    private Long leakDetectionThreshold;
    private Long maxLifetime;
    private Integer maxPoolSize;
    private Integer minIdle;

    private Long initializationFailTimeout;
    private String connectionInitSql;
    private String connectionTestQuery;
    private String dataSourceClassName;
    private String dataSourceJndiName;
    private String transactionIsolationName;
    private Boolean isAutoCommit;
    private Boolean isReadOnly;
    private Boolean isIsolateInternalQueries;
    private Boolean isRegisterMbeans;
    private Boolean isAllowPoolSuspension;
    private Properties dataSourceProperties;
    private Properties healthCheckProperties;
    
    private String schema;
    private String exceptionOverrideClassName;
    private Long keepaliveTime;
    private Boolean sealed;
}

@ConfigurationProperties(prefix = DynamicProperties.PREFIX)该注解表明该类是一个Properties配置类。该注解需要@EnableConfigurationProperties(DynamicProperties.class)配合才能使用,我们会在后面的自动装配类中使用@EnableConfigurationProperties注解。
对boot比较熟悉的小伙伴们肯定知道这个注解,有时候我们会将application.yml中信息通过配置类得到。
该类配置完之后,我们就可以在application.yml中设置我们的动态数据源配置,如下所示

spring:
  datasource:
    dynamic:
      datasource:
        one:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8
        second:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&useSSL=false&characterEncoding=utf8
      druid:
        initialSize: 10
        maxActive: 50
        minIdle: 5
        maxWait: 60
        minEvictableIdleTimeMillis: 30000
        maxEvictableIdleTimeMillis: 30000
      primary: one
      type: com.alibaba.druid.pool.DruidDataSource

这里数据库类型就仅仅支持德鲁伊跟HikariCp两种连接池

这里提供一个小知识,用过boot的小伙伴们都知道,application.yml有提示功能,那么我们自己定义的有没有提示功能的,这个是可以有的。我们可以引入一个依赖,自动生成yml文件的提示JSON文件。

        
            org.springframework.boot
            spring-boot-configuration-processor
        

这样在项目打包后,会根据配置类创建一个元数据JSON文件


这样我们在引入自己的Strater之后,就可以使用yml的自动提示功能。

6,定义AutoConfiguration
package com.xzq.dynamic;

@EnableConfigurationProperties(DynamicProperties.class)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {

    private final DynamicProperties properties;
    
    
    public DynamicDataSourceAutoConfiguration(DynamicProperties properties) {
        this.properties = properties;
    }
   	@Bean
  	public DsAspect dsAspect() {
        return new DsAspect();
    }

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        routingDataSource.setPrimaryKey(properties.getPrimary());
        //routingDataSource.setDataSourceMap(dataSourceMap);
        return routingDataSource;
    }
}

springBoot有约定大于配置的说法,我们写完这个配置类之后,如何装载到spring容器中呢?
约定来了,springBoot约定会去/resource/meta-INF/ 该目录下寻找一个spring.factories文件,该文件中定义了我们的配置类,spring就会默认将该配置类注入spring容器中。
所以我们需要自定义一个spring约定文件放在/resource/meta-INF/ 下

这样当我们的Starter被引入时就会被spring扫描到并将配置类注册到容器中。

我们基本上已经完成了该项目的骨架了。但是还缺少了重要的一步,我们需要在自动装配类中将数据源注入容器。
那么如何创建数据源?
又如何注入到容器中呢?
创建数据源我们肯定需要注册驱动,拿到driver-Class-Name,url,username,password等一些信息。这些信息在哪里呢?欸,就是用户输入到application.yml中的数据库信息了,前面我们定义了一个Properties配置类,可以拿到application.yml中的信息。所以说我们可以通过DynamicProperties来创建数据源。
那么创建数据源的方式该怎么做呢?
有啥可做的,仅说点废话,直接New一个DataSource,给他配置好属性一个@Bean注入到容器中,不就这点事吗?楼主一开始也是这样想的,也是这样做的。
看了苞米豆的源码后发现,卧槽,还能这样。这才是OOP!怪不得楼主只是一个码农。
下面来介绍如何创建数据源

6,定义DataSourceCreator创建器
package com.xzq.dynamic.creator;

public abstract class AbstractDataSourceCreator {

    protected final DynamicProperties properties;

    public AbstractDataSourceCreator(DynamicProperties properties) {
        this.properties = properties;
    }

    public DataSource createrDataSource(DataSourceProperty property) {
        return doCreateDataSource(property);
    }

    protected abstract DataSource doCreateDataSource(DataSourceProperty property);
}

这里还是老样子,定义抽象模板类,实际的创建方法交由子类实现

package com.xzq.dynamic.creator;

public class DruidDataSourceCreator extends AbstractDataSourceCreator implements DataSourceCreator{

    private Logger logger = LoggerFactory.getLogger(DruidDataSourceCreator.class);

    private final DruidConfig config;

    public DruidDataSourceCreator(DynamicProperties properties) {
        super(properties);
        config = properties.getDruid();
    }

    @Override
    protected DataSource doCreateDataSource(DataSourceProperty property) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(property.getDriverClassName());
        druidDataSource.setUsername(property.getUsername());
        druidDataSource.setPassword(property.getPassword());
        druidDataSource.setUrl(property.getUrl());
        Properties properties = config.toPropertes();
        druidDataSource.configFromPropety(properties);
        try {
            druidDataSource.init();
        } catch (SQLException e) {
            logger.error("druid init fail");
            new RuntimeException("druid init fail", e);
        }
        return druidDataSource;
    }
    @Override
    public boolean support(DataSourceProperty property) {
        Class type = properties.getType();
        return (type != null && DRUID_DATASOURCE.equals(type.getName()));
    }

}

package com.xzq.dynamic.creator;

public class HikariDataSourceCreator extends AbstractDataSourceCreator implements DataSourceCreator{

    private HikariCpConfig config;

    public HikariDataSourceCreator(DynamicProperties properties) {
        super(properties);
        config = properties.getHikari();
    }

    @Override
    protected DataSource doCreateDataSource(DataSourceProperty property) {
        HikariConfig config = new HikariConfig();
        BeanUtils.copyProperties(this.config, config);
        config.setUsername(property.getUsername());
        config.setPassword(property.getPassword());
        config.setJdbcUrl(property.getUrl());
        config.setDriverClassName(property.getDriverClassName());
        HikariDataSource hikariDataSource = new HikariDataSource(config);
        return hikariDataSource;
    }

    @Override
    public boolean support(DataSourceProperty property) {
        Class type = this.properties.getType();
        return (type!=null && DbConstants.HIKARI_DATASOURCE.equals(type.getName()));
    }
}

这里我只仅仅做了几个必要的连接池参数,其实德鲁伊跟hikariCp有很多连接池参数,有兴趣的小伙伴可以自己完善。

package com.xzq.dynamic.creator;
public class DefaultDataSourceCreator {
    private List creators;
    public DefaultDataSourceCreator(List creators) {
        this.creators = creators;
    }
    public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
        DataSourceCreator dataSourceCreator = null;
        for (DataSourceCreator creator : creators) {
            if (creator.support(dataSourceProperty)) {
                dataSourceCreator = creator;
                break;
            }
        }
        if (dataSourceCreator == null) {
            //使用默认德鲁伊
            dataSourceCreator = creators.get(0);
        }
        return dataSourceCreator.createrDataSource(dataSourceProperty);
    }
}

这里我们在搞一个默认创建器,该创建器的作用其实也就是拿到所有类型的创建器之后,然后去匹配用户在application.yml中设置的连接池类型来进行匹配,匹配上了就用该连接池的创建器创建数据源。
那么我们该如何导入到spring容器中呢,这里我们可以再定义一个创建器的自动装配类

package com.xzq.dynamic;
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {

    private final DynamicProperties properties;


    @Bean
    @ConditionalOnMissingBean
    public DefaultDataSourceCreator defaultDataSourceCreator(List creatorList) {
       return new DefaultDataSourceCreator(creatorList);
    }

    @Bean
    @ConditionalOnMissingBean
    public DruidDataSourceCreator druidDataSourceCreator() {
        return new DruidDataSourceCreator(properties);
    }

    @Bean
    @ConditionalOnMissingBean
    public HikariDataSourceCreator hikariDataSourceCreator() {
        return new HikariDataSourceCreator(properties);
    }
}

然后就是完善我们的动态数据源装配类

package com.xzq.dynamic;

@EnableConfigurationProperties(DynamicProperties.class)
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class,name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
@import(DynamicDataSourceCreatorAutoConfiguration.class)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {

    private final DynamicProperties properties;
    private final DefaultDataSourceCreator creator;
    private Map dataSourceMap = new ConcurrentHashMap<>();
    public DynamicDataSourceAutoConfiguration(DynamicProperties properties,DefaultDataSourceCreator creator) {
        this.properties = properties;
        this.creator = creator;
    }
    @Bean
    public DsAspect dsAspect() {
        return new DsAspect();
    }

    @Bean
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        routingDataSource.setPrimaryKey(properties.getPrimary());
        routingDataSource.setDataSourceMap(dataSourceMap);
        return routingDataSource;
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        properties.getDatasource().forEach((k,v)->{
            DataSource dataSource = creator.createDataSource(v);
            dataSourceMap.put(k, dataSource);
        });
    }
}

OK ,到这里我们基本上已经完成了动态数据源的Starter,接下来让我们进行测试来看一下还有什么问题某有。

四 实测准备 1,数据库准备

首先创建两个不同的库

create DATAbase test01;
create DATAbase test02;
2,表准备

准备两种表放在这两个库中

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `userId` varchar(9) DEFAULT NULL COMMENT '用户ID',
  `userNickName` varchar(32) DEFAULT NULL COMMENT '用户昵称',
  `userHead` varchar(16) DEFAULT NULL COMMENT '用户头像',
  `userPassword` varchar(64) DEFAULT NULL COMMENT '用户密码',
  `createTime` datetime DEFAULT NULL COMMENT '创建时间',
  `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;

INSERT INTO `test01`.`user` (`id`, `userId`, `userNickName`, `userHead`, `userPassword`, `createTime`, `updateTime`) VALUES ('1', '184172133', '小熊哥', '01_50', '123456', '2021-11-13 00:00:00', '2021-11-13 00:00:00');
INSERT INTO `test01`.`user` (`id`, `userId`, `userNickName`, `userHead`, `userPassword`, `createTime`, `updateTime`) VALUES ('2', '980765512', '你滴寒王', '02_50', '123456', '2021-11-13 00:00:00', '2021-11-13 00:00:00');
INSERT INTO `test01`.`user` (`id`, `userId`, `userNickName`, `userHead`, `userPassword`, `createTime`, `updateTime`) VALUES ('3', '796542178', 'EDG牛逼', '03_50', '123456', '2021-11-13 00:00:00', '2021-11-13 00:00:00');


CREATE TABLE `bs_city` (
  `id` varchar(40) NOT NULL,
  `name` varchar(255) NOT NULL,
  `create_time` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO `test02`.`bs_city` (`id`, `name`, `create_time`) VALUES ('1', '杭州', '2021-11-11 21:35:25');
INSERT INTO `test02`.`bs_city` (`id`, `name`, `create_time`) VALUES ('2', '郑州', '2021-11-11 21:35:36');
INSERT INTO `test02`.`bs_city` (`id`, `name`, `create_time`) VALUES ('3', '开封', '2021-11-11 21:35:42');
3,项目准备


创建一个springBoot项目,借助于苞米豆的代码生成器快速构建项目。
懒得搞的小伙伴可以去我的码云地址上拉下来直接用。顺便给个Star(那就更感激不尽了,哈哈)

五 实测
spring:
  datasource:
    dynamic:
      datasource:
        one:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/test01?useUnicode=true&useSSL=false&characterEncoding=utf8
        second:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/test02?useUnicode=true&useSSL=false&characterEncoding=utf8
      druid:
        initialSize: 10
        maxActive: 50
        minIdle: 5
        maxWait: 60
        minEvictableIdleTimeMillis: 30000
        maxEvictableIdleTimeMillis: 30000
      primary: one
      type: com.alibaba.druid.pool.DruidDataSource

这里我们定义了两个数据源,one和second。默认是One
我们创建一个Controller并对其方法加上Ds注解

package com.xzq.controller;
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private IUserService userService;
    @RequestMapping("/test2")
    @Ds("second")
    public Object test(String id) {
      return   userService.getById(id);
    }

}

启动项目测试

似乎看上去已经大功告成。
但是如果我们将Ds注解加到类上后,切面是不会对该类的方法进行拦截的。
那么为什么会这样呢?
首先我们先回顾一下我们的切面

可以看到我们的切入点表达式是注解类型的表达式。
看一下官网的介绍


可以看到切点表达式是针对方法的匹配
那我们在类上加上Ds注解,那么我们的切入点实现会根据切入点表达式去匹配方法是否拦截,而我们的方法由于没有加注解所以拦截不到。
那么PointCut的具体实现到底是什么呢? 匹配的逻辑又是什么呢?
其实PointCut切点中有两个重要的属性,分别是ClassFilter和MethodMatch

这是springAop中切点的接口定义。
那么这两个属性是干什么的呢,顾名思义,类过滤器就是针对类进行匹配,方法匹配器针对方法进行匹配。这两个肯定是先判断类符合规则不,如果符合在进行方法的匹配,先明确这一点。
那么是如何进行匹配的呢?没错就是切入点表达式,比如我们的切入点表达式是这样的
“@annotation(com.xzq.dynamic.annotation.Ds)”
那么类过滤器就是根据我们的目标类有没有这个注解进行过滤,显然我们的Ds标注在类上是符合的。
其次方法匹配器开始根据切入点表达式匹配方法有没有这个注解啊,欸,发现没有,那么就不拦截了。
以上只是我个人的一些猜想,那么实际情况是不是这样的呢?我们跟踪一下spring的aop的源码

这里就不展开对Aop的一长串跟踪了
其逻辑是这样的,首先开启@Aspectj 的自动代理,在项目启动后,AnnotationAwareAspectJAutoPr oxyCreator被注入到容器中,该类实现了BeanPostProcess,也就是说实现了对Bean的增强功能。
在每一个Bean的初始化方法前后,会调用AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInitialization方法和postProcessAfterInitialization方法,在postProcessAfterInitialization方法中会去获取所有的增强器,也就是去获取标注了@Aspect注解的类,然后将该类转成Advisor增强器,然后在拿着这些增强器匹配当前的Bean。如何匹配的最终会调用到上图所示的CanApply()方法,也就是是否可以使用的方法。
在这里先进行类匹配然后在方法匹配,匹配逻辑就是根据切入点表达式。
而我么你的Ds注解只作用在类上,没有作用在方法上,方法匹配器匹会匹配失败,所以造成拦截不住。
好了,重点来了,如何绕过方法匹配的匹配,直接类匹配上就不走方法匹配器,默认拦截该类的所有方法呢?
主要是通过spring提供的一个注解切点实现来绕过方法匹配器。
具体实现下次完善本文。

总结

这次一定一键三联!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/488915.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号