定义 Spring AOP 后无法切换数据库

Not able to switch database after defining Spring AOP

我正在研究多租户架构,我们必须处理数据库的动态创建和切换。

我面临的问题是我在 AOP 级别编写了切换数据库的整个代码,然后在控制器或服务级别我无法进行另一个切换。

AOPclass

/**
 * The type Api util.
 */
@Aspect
@Component
@Order(2)
public class APIUtil {
    private static final Logger log = LoggerFactory.getLogger(APIUtil.class);
    private final SchoolMasterService schoolMasterService;

    /**
     * Instantiates a new Api util.
     *
     * @param schoolMasterService the school master service
     * @param helperService
     */
    public APIUtil(SchoolMasterService schoolMasterService) {
        this.schoolMasterService = schoolMasterService;
    }

    /**
     * Around controller methods object.
     *
     * @param proceedingJoinPoint the proceeding join point
     * @return the object
     * @throws Throwable the throwable
     */
    @Around("execution(* com.example.board.controller.*.*(..))")
    public Object aroundControllerMethods(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("All schools loaded!");
        TenantContext.setCurrentTenant(DEFAULT_TENANT_ID);
        schoolMasterService.findAllMasters();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String tenantID = request.getHeader(XTENANTID).trim();
        return filterByHeader(tenantID, proceedingJoinPoint);
    }

   private Object filterByHeader(String tenantID, ProceedingJoinPoint joinPoint) throws Throwable {
        SchoolMaster schoolMaster = schoolMasterService.findBySchoolId(Long.parseLong(tenantID));
        log.info(format("Current school is %s", schoolMaster.getDataSourceKey()));
        TenantContext.setCurrentTenant(schoolMaster.getDataSourceKey());
        return joinPoint.proceed();
    }
}

CurrentTenantIdentifierResolverImpl class

@Component
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String resolveCurrentTenantIdentifier() {
        String currentDataSourceKey = TenantContext.getCurrentTenant();
        if (Objects.isNull(currentDataSourceKey)) {
            currentDataSourceKey = DEFAULT_TENANT_ID;
        }
        logger.debug("currentDataSourceKey {}", currentDataSourceKey);
        return currentDataSourceKey;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

MultiTenantConnectionProviderImpl class

@Component(MultiTenantConnectionProviderImpl.BEAN_ID)
public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
    /**
     * The constant BEAN_ID.
     */
    public static final String BEAN_ID = "multiTenantConnectionProvider";
    private static final long serialVersionUID = 7395318315512114572L;

    @Autowired
    private TenantDataSource tenantDataSource;

    private Logger log;

    /**
     * Instantiates a new Multi tenant connection provider.
     */
    public MultiTenantConnectionProviderImpl() {
        log = LoggerFactory.getLogger(getClass());
    }

    @Override
    protected DataSource selectAnyDataSource() {
        log.debug("selectAnyDataSource , returning dafault tenantid");
        return tenantDataSource.getDataSource(DEFAULT_TENANT_ID);
    }

    @Override
    protected DataSource selectDataSource(String tenantIdentifier) {
        log.debug("selected Datasource {} ", tenantIdentifier);
        return tenantDataSource.getDataSource(tenantIdentifier);
    }
}

TenantDataSource class

@Component
public class TenantDataSource {
    private final Map<Object, Object> tenantDataSourcesMap = new HashMap<>();
    private final Logger log = LoggerFactory.getLogger(TenantDataSource.class);

    private final DataSourceProperties dataSourceProperties;
    private final DataSource dataSource;
    private final JdbcTemplate jdbcTemplateObject;

    /**
     * Instantiates a new Tenant data source.
     *
     * @param properties the properties
     * @param source     the source
     * @param object     the object
     */
    public TenantDataSource(DataSourceProperties properties, DataSource source, JdbcTemplate object) {
        this.dataSourceProperties = properties;
        this.dataSource = source;
        this.jdbcTemplateObject = object;
    }

    /**
     * Add default datasource to map.
     */
    @PostConstruct
    void addDefaultDatasourceToMap() {
        tenantDataSourcesMap.put(DEFAULT_TENANT_ID, dataSource);
    }

    /**
     * Gets data source.
     *
     * @param dataSourceName the data source name
     * @return the data source
     */
    public DataSource getDataSource(String dataSourceName) {
        DataSource currentDatasource = null;
        log.debug("getDataSource().dataSourceName {}", dataSourceName);
        if (tenantDataSourcesMap.containsKey(dataSourceName)) {
            currentDatasource = (DataSource) tenantDataSourcesMap.get(dataSourceName);
        }
        return currentDatasource;
    }

    /**
     * Load tenant boolean.
     *
     * @param tenantDatasource the tenant datasource
     * @return the boolean
     */
    public boolean loadTenant(SchoolMaster tenantDatasource) {
        try {
            if (!verifyPort(tenantDatasource))
                return false;
            DataSource temp = createDataSource(tenantDatasource);
            boolean result = verifyConnection(temp);
            if (result) {
                tenantDataSourcesMap.putIfAbsent(tenantDatasource.getDataSourceKey(), temp);
            }
            return result;
        } catch (Exception h) {
            return false;
        }
    }

    /**
     * Load all tenants.
     *
     * @param tenantDatasourcesList the tenant datasources list
     */
    public void loadAllTenants(List<SchoolMaster> tenantDatasourcesList) {
        tenantDatasourcesList.forEach(tenant -> tenantDataSourcesMap.putIfAbsent(tenant.getDataSourceKey(), createDataSource(tenant)));
    }

    /**
     * Create data source data source.
     *
     * @param tenantDatasource the tenant datasource
     * @return the data source
     */
    public DataSource createDataSource(SchoolMaster tenantDatasource) {
        HikariDataSource hikariDataSource = null;
        if (Objects.nonNull(tenantDatasource)) {
            String url = JDBCMYSQL + tenantDatasource.getSchoolIP().trim() + ":" + tenantDatasource.getDataSourcePort().trim() + SLASH + tenantDatasource.getDataSourceKey().trim() + "?createDatabaseIfNotExist=true&useSSL=true";
            hikariDataSource = (HikariDataSource) DataSourceBuilder.create()
                    .driverClassName(dataSourceProperties.getDriverClassName())
                    .username(tenantDatasource.getDataSourceUserName()).password(tenantDatasource.getDataSourcePassword())
                    .url(url)
                    .build();
            setConnectionPooling(hikariDataSource);
        }
        return hikariDataSource;
    }

    /**
     * Create schema.
     *
     * @param dataSourceName the data source name
     * @throws SQLException the sql exception
     */
    public void createSchema(String dataSourceName) throws SQLException {
        if (tenantDataSourcesMap.containsKey(dataSourceName)) {
            jdbcTemplateObject.execute(CREATE_SCHEMA + " " + dataSourceName);
            jdbcTemplateObject.execute(USE_SCHEMA + " " + dataSourceName);
            DataSource currentDataSource = (DataSource) tenantDataSourcesMap.get(dataSourceName);
            ClassPathResource resource = new ClassPathResource("dbscripts/schema.sql");
            try (Connection connection = currentDataSource.getConnection()) {
                ScriptUtils.executeSqlScript(connection, new EncodedResource(resource, "UTF-8"));
            }
            jdbcTemplateObject.execute(USE_SCHEMA + " " + DEFAULT_TENANT_ID);
        }
    }

    /**
     * Drop schema.
     *
     * @param dataSourceName the data source name
     */
    public void dropSchema(String dataSourceName) {
        if (tenantDataSourcesMap.containsKey(dataSourceName)) {
            DataSource currentDataSource = (DataSource) tenantDataSourcesMap.get(dataSourceName);
            JdbcTemplate template = new JdbcTemplate(currentDataSource);
            template.execute(DROP_SCHEMA + " " + dataSourceName);
        }
    }

    /**
     * Sets connection pooling.
     *
     * @param hikariDataSource the hikari data source
     */
    private void setConnectionPooling(HikariDataSource hikariDataSource) {
        hikariDataSource.setMinimumIdle(2);
        hikariDataSource.setMaximumPoolSize(5);
        hikariDataSource.setIdleTimeout(100000);
        hikariDataSource.setMaxLifetime(3000000);
        hikariDataSource.setConnectionTimeout(200000);
        hikariDataSource.setLeakDetectionThreshold(2100);
        hikariDataSource.setConnectionTestQuery("SELECT 1 FROM DUAL");
        hikariDataSource.setAutoCommit(false);
    }


    /**
     * Verify connection boolean.
     *
     * @param currentDatasource the current datasource
     * @return the boolean
     */
    private boolean verifyConnection(DataSource currentDatasource) {
        try (Connection ignored = currentDatasource.getConnection()) {
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * Verify port boolean.
     *
     * @param tenantDataSource the tenant data source
     * @return the boolean
     */
    private boolean verifyPort(SchoolMaster tenantDataSource) {
        return tenantDataSource.getDataSourcePort().trim().chars().allMatch(Character::isDigit);
    }
}

TenantContext class

public final class TenantContext {
    private static final ThreadLocal<String> currentTenant = ThreadLocal.withInitial(() -> DEFAULT_TENANT_ID);

    private TenantContext() {
    }

    /**
     * Gets current tenant.
     *
     * @return the current tenant
     */
    public static String getCurrentTenant() {
        return currentTenant.get();
    }

    /**
     * Sets current tenant.
     *
     * @param tenant the tenant
     */
    public static void setCurrentTenant(String tenant) {
        currentTenant.set(tenant);
    }

    /**
     * Clear.
     */
    public static void clear() {
        currentTenant.remove();
    }
}

在 controller/service 级别,我无法使用 TenantContext.setCurrentTenant(String identifier) 切换数据库 但是我可以在 AOP 上做同样的事情。

这是什么原因?有什么办法可以解决这个问题吗?

如果有任何帮助,我将不胜感激。

我添加了服务代码示例

我需要切换数据库两次,这是不可能的。

 @Transactional(readOnly = true)
    @Override
    public List<SyllabusListingResponseDto> getSyllabusByBoardId(Long rackId, Long languageId) {
        logger.info("getSyllabusByBoardId Method called in BoardManagementServiceImpl");
        ResourceRackModel resourceRackModel = resourceRackService.getByRackIdAndStatus(rackId, ACTIVE_STATUS);
        if (Objects.nonNull(resourceRackModel)) {
            TenantContext.setCurrentTenant(DEFAULT_TENANT_ID);
            List<Long> rackIds = resourceRackService.findAllRackIdsByBoardId(rackId);
            rackIds.add(rackId);
            ResourceRackModel boardModel = resourceRackModel;
            if (!boardModel.getParentPath().isEmpty()) {
                String[] ids = resourceRackModel.getParentPath().split(",", 2);
                boardModel = resourceRackService.getByRackIdAndStatus(Long.parseLong(ids[INT_ZERO]), ACTIVE_STATUS);
            }
            TenantContext.setCurrentTenant("S_" + 1);
            BoardVersionModel activeVersionModel = boardVersionRepository.findByBoardIdAndStatusAndVersionStatus(boardModel.getRackId(), ACTIVE_STATUS, ACTIVE_STATUS);
            ContentCountDto contentCountDto = new ContentCountDto().setStatus(true).setRackIds(rackIds).setActiveBoardVersionId(activeVersionModel.getVersionId().toString());
            ResponseModel responseModel = nemrSTCManagementClient.findContentCount(Common.getTenantId(), contentCountDto).getBody();
            if (Objects.nonNull(responseModel)) {
                Map<String, List<String>> lookup = (Map<String, List<String>>) responseModel.getObject();
                String languageCode = languageMasterService.findByIdAndStatus(languageId, ACTIVE_STATUS).getLanguageCode();
                String defaultLanguageCode = languageMasterService.findByIdAndStatus(resourceRackModel.getDefaultLanguageId(), ACTIVE_STATUS).getLanguageCode();
                List<ResourceRackModel> resourceRackModels = resourceRackService.findByParentIdAndStatus(rackId, ACTIVE_STATUS);
                if (resourceRackModels.isEmpty()) {
                    return Collections.emptyList();
                }
                Map<Integer, String> rackTypes = new HashMap<>();
                return getResult(languageId, lookup, new SyllabusListingResponseDto(), boardModel, languageCode, defaultLanguageCode, resourceRackModels, rackTypes);
            }
        }
        return Collections.emptyList();
    }

  1. 因为我没有使用过MultiTenantConnectionProvider所以不确定框架在什么时候选择了datasource

  2. 但我非常怀疑它是由@Transactional创建的interceptor自动完成的,而框架从不读取方法内部的TenantContext。即一种交易方法的一个数据源。 所以可能值得确定我的怀疑是否属实。

    • 您可以在以下位置设置断点:
        logger.info("getSyllabusByBoardId Method called in BoardManagement...");
    
    • 然后在断点处清除控制台日志。

    • 然后让该方法执行并查看在单步执行服务方法行时是否打印了以下任何日志。

        log.debug("selectAnyDataSource , returning dafault tenantid");
    
       log.debug("selected Datasource {} ", tenantIdentifier);
    
  3. 如果我在第 2 步中的怀疑是正确的,那么您要么必须删除 @Transactional 注释,要么将方法一分为二并为每个方法设置正确的 TenantContext.setCurrentTenant在调用服务方法之前在控制器中。