Spring @Transactional не работает с InvocationHandler - PullRequest
0 голосов
/ 14 октября 2019

У меня есть цепочка прокси для JDBC Connection, PreparedStatement и Statement.

ConnectionProxy:

public class HikariConnectionProxy implements InvocationHandler {
    private final HikariDataSource dataSource;
    private final Connection delegate;
    private boolean committed;

    public static Connection newInstance(HikariDataSource dataSource) throws SQLException {
        final Connection connection = dataSource.getConnection();
        return (Connection) Proxy.newProxyInstance(
                connection.getClass().getClassLoader(),
                connection.getClass().getInterfaces(),
                new HikariConnectionProxy(dataSource, connection)
        );
    }

    public static Connection newInstance(HikariDataSource dataSource, String username, String password) throws SQLException {
        final Connection connection = dataSource.getConnection(username, password);
        return (Connection) Proxy.newProxyInstance(
                connection.getClass().getClassLoader(),
                connection.getClass().getInterfaces(),
                new HikariConnectionProxy(dataSource, connection)
        );
    }

    private HikariConnectionProxy(HikariDataSource dataSource, Connection connection) throws SQLException {
        this.dataSource = dataSource;
        delegate = connection;
        checkAutoCommit();
    }

    private void checkAutoCommit() throws SQLException {
        if (!delegate.getAutoCommit()) {
            delegate.setAutoCommit(true);
        }
    }

    private void commit() throws SQLException {
        delegate.commit();
        committed = true;
    }

    private void close() throws SQLException {
      // some code here
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Void.TYPE == method.getReturnType()) {
            final String methodName = method.getName().toLowerCase();
            if ("close".equals(methodName)) {
                this.close();
                return null;
            } else if ("commit".equals(methodName)) {
                this.commit();
                return null;
            }
        } else if (PreparedStatement.class == method.getReturnType()) {
            return ServiceBusPreparedStatementProxy.newInstance((PreparedStatement) method.invoke(delegate, args));
        } else if (Statement.class == method.getReturnType()) {
            return ServiceBusStatementProxy.newInstance((Statement) method.invoke(delegate, args));
        }
        return method.invoke(delegate, args);
    }
}

StatementProxy:

public class ServiceBusStatementProxy implements InvocationHandler {
    final boolean toCheckUnsigned;
    final Statement delegate;

    public static Statement newInstance(Statement statement) throws SQLException {
        return (Statement) Proxy.newProxyInstance(
                statement.getClass().getClassLoader(),
                statement.getClass().getInterfaces(),
                new ServiceBusStatementProxy(statement, statement.getConnection().getMetaData().getDatabaseProductName())
        );
    }

    ServiceBusStatementProxy(Statement statement, String dbName) {
        String dbNameLowerCase = dbName.toLowerCase();
        toCheckUnsigned = dbNameLowerCase.contains("oracle") || dbNameLowerCase.contains("mysql") || dbNameLowerCase.contains("mariadb");
        delegate = statement;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return toCheckUnsigned && ResultSet.class == method.getReturnType()
                ? ServiceBusResultSetProxy.newInstance((ResultSet) method.invoke(delegate, args))
                : method.invoke(delegate, args);
    }
}

PreparedStatementProxy:

public class ServiceBusPreparedStatementProxy extends ServiceBusStatementProxy implements InvocationHandler {
    private static final Predicate<Runnable> isValidArithmetic = runnable -> {
        try {
            runnable.run();
            return true;
        } catch (ArithmeticException e) {
            return false;
        }
    };

    public static PreparedStatement newInstance(PreparedStatement preparedStatement) throws SQLException {
        return (PreparedStatement) Proxy.newProxyInstance(
                preparedStatement.getClass().getClassLoader(),
                preparedStatement.getClass().getInterfaces(),
                new ServiceBusPreparedStatementProxy(preparedStatement, preparedStatement.getConnection().getMetaData().getDatabaseProductName())
        );
    }

    private ServiceBusPreparedStatementProxy(Statement statement, String dbName) {
        super(statement, dbName);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (toCheckUnsigned && Void.TYPE == method.getReturnType()
                && args != null && args.length >= 3 && "setObject".equalsIgnoreCase(method.getName()) && args[1] != null) {
            Class<?> valueClass = args[1].getClass();
            if (BigInteger.class == valueClass) {
                BigInteger bigInt = (BigInteger) args[1];
                if (!isValidArithmetic.test(bigInt::longValueExact)) {
                    ((PreparedStatement) delegate).setString((Integer) args[0], bigInt.toString());
                    return null;
                }
            }
        }
        return super.invoke(proxy, method, args);
    }
}

Когда я использую это соединение с методом с аннотацией @Transactional, транзакция не работает. Когда я удалил этот прокси-сервер, все прошло хорошо с тем же методом @Transactional.

Есть ли какое-либо объяснение этому и как это исправить?

Использование этого с этим источником данных:

@Service
public class BaseHikariDataSourceFactory implements DataSourceFactory {

    @Value("${hikari.leak.detection.threshold:0}")
    private int leakDetectionThreshold;
    @Value("${hikari.minimum.idle:0}")
    private int minimumIdle;
    @Value("${hikari.idle.timeout:60000}")
    private int idleTimeout;
    @Value("${hikari.maximum.pool.size:10}")
    private int maximumPoolSize;

    @Override
    public DataSource createHikariDataSource(BaseHikariDataSourceParams params) {
        HikariConfig config = new HikariConfig();
        ExternalSystemParams externalSystemParams = params.getExternalSystemParams();

        config.setPoolName(params.getPoolName());
        config.setJdbcUrl(params.getJdbcUrl());
        config.setDriverClassName(externalSystemParams.getDriverClassName());
        config.setUsername(externalSystemParams.getUsername());
        config.setPassword(externalSystemParams.getPassword());
        config.setMaximumPoolSize(Optional.ofNullable(externalSystemParams.getMaximumPoolSize()).orElse(maximumPoolSize));
        config.setIdleTimeout(Optional.ofNullable(externalSystemParams.getIdleTimeout()).orElse(idleTimeout));
        config.setMinimumIdle(Optional.ofNullable(externalSystemParams.getMinimumIdle()).orElse(minimumIdle));
        config.setLeakDetectionThreshold(leakDetectionThreshold);
        config.setRegisterMbeans(true);
        if (externalSystemParams.getConnectionTestQuery() != null) {
            config.setConnectionTestQuery(externalSystemParams.getConnectionTestQuery());
        }
        if (externalSystemParams.getValidationTimeout() != null) {
            config.setValidationTimeout(externalSystemParams.getValidationTimeout());
        }
        return new HikariDataSourceProxyConnection(config);
    }

    @Override
    public DataSource createHikariDataSource(Share share, boolean isWorker) {
        HikariConfig config = new HikariConfig();
        config.setPoolName("Hikari CP " + (isWorker ? share.getAlias() + WORKERS_DS_POSTFIX : share.getAlias()));
        config.setDriverClassName(share.getDriver());
        config.setJdbcUrl(share.getUrl());
        config.setUsername(share.getLogin());
        config.setPassword(share.getPassword());
        config.setMaximumPoolSize(isWorker ? share.getMaxPoolSizeWorkers() : share.getMaxPoolSize());
        config.addDataSourceProperty("useCompression", share.isUseCompression());
        config.setLeakDetectionThreshold(leakDetectionThreshold);
        config.setRegisterMbeans(true);
        return new HikariDataSourceProxyConnection(config);
    }

    private static class HikariDataSourceProxyConnection extends HikariDataSource {
        private final HikariDataSource delegate;

        HikariDataSourceProxyConnection(HikariConfig configuration) {
            delegate = new HikariDataSource(configuration);
        }

        @Override
        public Connection getConnection() throws SQLException {
            return HikariConnectionProxy.newInstance(delegate);
        }

        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return HikariConnectionProxy.newInstance(delegate, username, password);
        }
    }
}
...