feat: support custom database types

This commit is contained in:
vran
2022-03-11 23:13:56 +08:00
parent 91c3b0184d
commit 0333ecc8ae
26 changed files with 719 additions and 41 deletions

View File

@@ -0,0 +1,32 @@
package com.databasir.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
// 支持中文编码
restTemplate.getMessageConverters().set(1,
new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(1000 * 30);//单位为ms
factory.setConnectTimeout(1000 * 5);//单位为ms
return factory;
}
}

View File

@@ -27,7 +27,11 @@ public enum DomainErrors implements DatabasirErrors {
INVALID_CRON_EXPRESSION("A_10012", "不合法的 cron 表达式"),
REGISTRATION_ID_DUPLICATE("A_10013", "应用注册 ID 不能重复"),
REGISTRATION_ID_NOT_FOUND("A_10014", "应用 ID 不存在"),
MISS_REQUIRED_PARAMETERS("A_10015", "缺少必填参数");
MISS_REQUIRED_PARAMETERS("A_10015", "缺少必填参数"),
DATABASE_TYPE_NAME_DUPLICATE("A_10016", "数据库类型名已存在"),
MUST_NOT_MODIFY_SYSTEM_DEFAULT_DATABASE_TYPE("A_10017", "禁止修改系统默认数据库类型"),
DOWNLOAD_DRIVER_ERROR("A_10018", "驱动下载失败"),
;
private final String errCode;
@@ -46,6 +50,6 @@ public enum DomainErrors implements DatabasirErrors {
}
public DatabasirException exception(String s) {
return exception(s, (Throwable) null);
return exception(s, null);
}
}

View File

@@ -0,0 +1,20 @@
package com.databasir.core.domain.database.converter;
import com.databasir.core.domain.database.data.DatabaseTypeCreateRequest;
import com.databasir.core.domain.database.data.DatabaseTypeDetailResponse;
import com.databasir.core.domain.database.data.DatabaseTypePageResponse;
import com.databasir.core.domain.database.data.DatabaseTypeUpdateRequest;
import com.databasir.dao.tables.pojos.DatabaseTypePojo;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface DatabaseTypePojoConverter {
DatabaseTypePojo of(DatabaseTypeCreateRequest request);
DatabaseTypePojo of(DatabaseTypeUpdateRequest request);
DatabaseTypeDetailResponse toDetailResponse(DatabaseTypePojo data);
DatabaseTypePageResponse toPageResponse(DatabaseTypePojo pojo, Integer projectCount);
}

View File

@@ -0,0 +1,26 @@
package com.databasir.core.domain.database.data;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class DatabaseTypeCreateRequest {
@NotBlank
private String databaseType;
private String icon;
@NotBlank
private String description;
@NotBlank
private String jdbcDriverFileUrl;
@NotBlank
private String jdbcDriverClassName;
@NotBlank
private String jdbcProtocol;
}

View File

@@ -0,0 +1,31 @@
package com.databasir.core.domain.database.data;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DatabaseTypeDetailResponse {
private Integer id;
private String databaseType;
private String icon;
private String description;
private String jdbcDriverFileUrl;
private String jdbcDriverClassName;
private String jdbcProtocol;
private Boolean deleted;
private Integer deletedToken;
private LocalDateTime updateAt;
private LocalDateTime createAt;
}

View File

@@ -0,0 +1,26 @@
package com.databasir.core.domain.database.data;
import com.databasir.dao.Tables;
import lombok.Data;
import org.jooq.Condition;
import org.jooq.impl.DSL;
import java.util.ArrayList;
import java.util.List;
@Data
public class DatabaseTypePageCondition {
private String databaseTypeContains;
public Condition toCondition() {
List<Condition> conditions = new ArrayList<>();
conditions.add(Tables.DATABASE_TYPE.DELETED.eq(false));
if (databaseTypeContains != null && !databaseTypeContains.trim().equals("")) {
conditions.add(Tables.DATABASE_TYPE.DATABASE_TYPE_.containsIgnoreCase(databaseTypeContains));
}
return conditions.stream()
.reduce(Condition::and)
.orElse(DSL.trueCondition());
}
}

View File

@@ -0,0 +1,33 @@
package com.databasir.core.domain.database.data;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DatabaseTypePageResponse {
private Integer id;
private String databaseType;
private String icon;
private String description;
private String jdbcDriverFileUrl;
private String jdbcDriverClassName;
private String jdbcProtocol;
private Boolean deleted;
private Integer deletedToken;
private Integer projectCount;
private LocalDateTime updateAt;
private LocalDateTime createAt;
}

View File

@@ -0,0 +1,30 @@
package com.databasir.core.domain.database.data;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class DatabaseTypeUpdateRequest {
@NotNull
private Integer id;
@NotBlank
private String databaseType;
private String icon;
@NotBlank
private String description;
@NotBlank
private String jdbcDriverFileUrl;
@NotBlank
private String jdbcDriverClassName;
@NotBlank
private String jdbcProtocol;
}

View File

@@ -0,0 +1,103 @@
package com.databasir.core.domain.database.service;
import com.databasir.core.domain.DomainErrors;
import com.databasir.core.domain.database.converter.DatabaseTypePojoConverter;
import com.databasir.core.domain.database.data.*;
import com.databasir.core.infrastructure.connection.DatabaseTypes;
import com.databasir.core.infrastructure.driver.DriverResources;
import com.databasir.dao.impl.DatabaseTypeDao;
import com.databasir.dao.impl.ProjectDao;
import com.databasir.dao.tables.pojos.DatabaseTypePojo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class DatabaseTypeService {
private final DriverResources driverResources;
private final DatabaseTypeDao databaseTypeDao;
private final ProjectDao projectDao;
private final DatabaseTypePojoConverter databaseTypePojoConverter;
public Integer create(DatabaseTypeCreateRequest request) {
DatabaseTypePojo pojo = databaseTypePojoConverter.of(request);
try {
return databaseTypeDao.insertAndReturnId(pojo);
} catch (DuplicateKeyException e) {
throw DomainErrors.DATABASE_TYPE_NAME_DUPLICATE.exception();
}
}
@Transactional
public void update(DatabaseTypeUpdateRequest request) {
databaseTypeDao.selectOptionalById(request.getId()).ifPresent(data -> {
if (DatabaseTypes.has(data.getDatabaseType())) {
throw DomainErrors.MUST_NOT_MODIFY_SYSTEM_DEFAULT_DATABASE_TYPE.exception();
}
DatabaseTypePojo pojo = databaseTypePojoConverter.of(request);
try {
databaseTypeDao.updateById(pojo);
} catch (DuplicateKeyException e) {
throw DomainErrors.DATABASE_TYPE_NAME_DUPLICATE.exception();
}
// 名称修改,下载地址修改需要删除原有的 driver
if (!Objects.equals(request.getDatabaseType(), data.getDatabaseType())
|| !Objects.equals(request.getJdbcDriverFileUrl(), data.getJdbcDriverFileUrl())) {
driverResources.delete(data.getDatabaseType());
}
});
}
public void deleteById(Integer id) {
databaseTypeDao.selectOptionalById(id).ifPresent(data -> {
if (DatabaseTypes.has(data.getDatabaseType())) {
throw DomainErrors.MUST_NOT_MODIFY_SYSTEM_DEFAULT_DATABASE_TYPE.exception();
}
databaseTypeDao.deleteById(id);
driverResources.delete(data.getDatabaseType());
});
}
public Page<DatabaseTypePageResponse> findByPage(Pageable page,
DatabaseTypePageCondition condition) {
Page<DatabaseTypePojo> pageData = databaseTypeDao.selectByPage(page, condition.toCondition());
List<String> databaseTypes = pageData.map(DatabaseTypePojo::getDatabaseType).toList();
Map<String, Integer> projectCountMapByDatabaseType = projectDao.countByDatabaseTypes(databaseTypes);
return pageData
.map(data -> {
Integer count = projectCountMapByDatabaseType.getOrDefault(data.getDatabaseType(), 0);
return databaseTypePojoConverter.toPageResponse(data, count);
});
}
public List<String> listSimpleDatabaseTypes() {
return databaseTypeDao.selectAll()
.stream()
.map(DatabaseTypePojo::getDatabaseType)
.collect(Collectors.toList());
}
public Optional<DatabaseTypeDetailResponse> selectOne(Integer id) {
return databaseTypeDao.selectOptionalById(id)
.map(databaseTypePojoConverter::toDetailResponse);
}
}

View File

@@ -35,6 +35,7 @@ public @interface Operation {
String GROUP = "group";
String LOGIN_APP = "login_app";
String SETTING = "setting";
String DATABASE_TYPE = "database_type";
}
interface Types {

View File

@@ -0,0 +1,80 @@
package com.databasir.core.infrastructure.connection;
import com.databasir.core.domain.DomainErrors;
import com.databasir.core.infrastructure.driver.DriverResources;
import com.databasir.dao.impl.DatabaseTypeDao;
import com.databasir.dao.tables.pojos.DatabaseTypePojo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.util.Properties;
@Component
@RequiredArgsConstructor
@Slf4j
public class CustomDatabaseConnectionFactory implements DatabaseConnectionFactory {
private final DatabaseTypeDao databaseTypeDao;
private final DriverResources driverResources;
@Override
public boolean support(String databaseType) {
return databaseTypeDao.existsByDatabaseType(databaseType);
}
@Override
public Connection getConnection(Context context) throws SQLException {
DatabaseTypePojo type = databaseTypeDao.selectByDatabaseType(context.getDatabaseType());
File driverFile;
try {
driverFile = driverResources.download(context.getDatabaseType(), type.getJdbcDriverFileUrl());
} catch (IOException e) {
log.error("download driver error " + context, e);
throw DomainErrors.DOWNLOAD_DRIVER_ERROR.exception(e.getMessage());
}
URLClassLoader loader = null;
try {
loader = new URLClassLoader(
new URL[]{
driverFile.toURI().toURL()
},
this.getClass().getClassLoader()
);
} catch (MalformedURLException e) {
log.error("load driver error " + context, e);
throw DomainErrors.CONNECT_DATABASE_FAILED.exception(e.getMessage());
}
Class<?> clazz = null;
Driver driver = null;
try {
clazz = Class.forName(type.getJdbcDriverClassName(), true, loader);
driver = (Driver) clazz.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
log.error("init driver error", e);
throw DomainErrors.CONNECT_DATABASE_FAILED.exception("驱动初始化异常, 请检查 Driver name" + e.getMessage());
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
log.error("init driver error", e);
throw DomainErrors.CONNECT_DATABASE_FAILED.exception("驱动初始化异常:" + e.getMessage());
}
Properties info = new Properties();
info.put("user", context.getUsername());
info.put("password", context.getPassword());
String jdbcUrl = type.getJdbcProtocol() + "://" + context.getUrl() + "/" + context.getSchema();
return driver.connect(jdbcUrl, info);
}
}

View File

@@ -1,5 +1,8 @@
package com.databasir.core.infrastructure.connection;
import lombok.Builder;
import lombok.Data;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
@@ -8,9 +11,22 @@ public interface DatabaseConnectionFactory {
boolean support(String databaseType);
Connection getConnection(String username,
String password,
String url,
String schema,
Properties properties) throws SQLException;
Connection getConnection(Context context) throws SQLException;
@Builder
@Data
class Context {
private String databaseType;
private String username;
private String password;
private String url;
private String schema;
private Properties properties;
}
}

View File

@@ -32,11 +32,19 @@ public class DatabaseConnectionService {
Properties info = new Properties();
dataSourceProperties.forEach(prop -> info.put(prop.getKey(), prop.getValue()));
try {
DatabaseConnectionFactory.Context context = DatabaseConnectionFactory.Context.builder()
.username(username)
.password(password)
.url(url)
.schema(dataSource.getDatabaseName())
.properties(info)
.databaseType(dataSource.getDatabaseType())
.build();
return factories.stream()
.filter(factory -> factory.support(dataSource.getDatabaseType()))
.findFirst()
.orElseThrow(DomainErrors.NOT_SUPPORT_DATABASE_TYPE::exception)
.getConnection(username, password, url, dataSource.getDatabaseName(), info);
.getConnection(context);
} catch (SQLException e) {
throw DomainErrors.CONNECT_DATABASE_FAILED.exception(e.getMessage(), e);
}
@@ -49,11 +57,19 @@ public class DatabaseConnectionService {
String databaseType,
Properties properties) {
try {
DatabaseConnectionFactory.Context context = DatabaseConnectionFactory.Context.builder()
.username(username)
.password(password)
.url(url)
.schema(databaseName)
.properties(properties)
.databaseType(databaseType)
.build();
factories.stream()
.filter(factory -> factory.support(databaseType))
.findFirst()
.orElseThrow(DomainErrors.NOT_SUPPORT_DATABASE_TYPE::exception)
.getConnection(username, password, url, databaseName, properties);
.getConnection(context);
} catch (SQLException e) {
throw DomainErrors.CONNECT_DATABASE_FAILED.exception(e.getMessage(), e);
}

View File

@@ -1,10 +1,19 @@
package com.databasir.core.infrastructure.connection;
import java.util.Objects;
public interface DatabaseTypes {
String MYSQL = "mysql";
String POSTGRESQL = "postgresql";
String ORACLE = "oracle";
static boolean has(String name) {
if (name == null) {
return false;
}
return Objects.equals(MYSQL, name.toLowerCase())
|| Objects.equals(POSTGRESQL, name.toLowerCase());
}
}

View File

@@ -16,11 +16,7 @@ public class MysqlDatabaseConnectionFactory implements DatabaseConnectionFactory
}
@Override
public Connection getConnection(String username,
String password,
String url,
String schema,
Properties properties) throws SQLException {
public Connection getConnection(Context context) throws SQLException {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
@@ -28,10 +24,10 @@ public class MysqlDatabaseConnectionFactory implements DatabaseConnectionFactory
}
Properties info = new Properties();
info.put("user", username);
info.put("password", password);
info.putAll(properties);
String jdbcUrl = "jdbc:mysql://" + url + "/" + schema;
info.put("user", context.getUsername());
info.put("password", context.getPassword());
info.putAll(context.getProperties());
String jdbcUrl = "jdbc:mysql://" + context.getUrl() + "/" + context.getSchema();
return DriverManager.getConnection(jdbcUrl, info);
}

View File

@@ -16,11 +16,7 @@ public class PostgresqlDatabaseConnectionFactory implements DatabaseConnectionFa
}
@Override
public Connection getConnection(String username,
String password,
String url,
String schema,
Properties properties) throws SQLException {
public Connection getConnection(Context context) throws SQLException {
try {
Class.forName("org.postgresql.Driver");
} catch (ClassNotFoundException e) {
@@ -28,10 +24,10 @@ public class PostgresqlDatabaseConnectionFactory implements DatabaseConnectionFa
}
Properties info = new Properties();
info.put("user", username);
info.put("password", password);
info.putAll(properties);
String jdbcUrl = "jdbc:postgresql://" + url + "/" + schema;
info.put("user", context.getUsername());
info.put("password", context.getPassword());
info.putAll(context.getProperties());
String jdbcUrl = "jdbc:postgresql://" + context.getUrl() + "/" + context.getSchema();
return DriverManager.getConnection(jdbcUrl, info);
}
}

View File

@@ -0,0 +1,84 @@
package com.databasir.core.infrastructure.driver;
import com.databasir.core.domain.DomainErrors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Component
@Slf4j
@RequiredArgsConstructor
public class DriverResources {
@Value("${databasir.db.driver-directory}")
private String driverBaseDirectory;
private final RestTemplate restTemplate;
public File download(String databaseType, String driverFileUrl) throws IOException {
// create parent directory
if (Files.notExists(Path.of(driverBaseDirectory))) {
Files.createDirectory(Path.of(driverBaseDirectory));
}
String filePath = driverPath(databaseType);
Path path = Path.of(filePath);
if (Files.exists(path)) {
// ignore
log.debug("{} already exists, ignore download from {}", filePath, driverFileUrl);
return path.toFile();
} else {
// download
try {
return restTemplate.execute(driverFileUrl, HttpMethod.GET, null, response -> {
if (response.getStatusCode().is2xxSuccessful()) {
File file = path.toFile();
StreamUtils.copy(response.getBody(), new FileOutputStream(file));
log.info("{} download success ", filePath);
return file;
} else {
log.error("{} download error from {}: {} ", filePath, driverFileUrl, response);
throw DomainErrors.DOWNLOAD_DRIVER_ERROR.exception("驱动下载失败:"
+ response.getStatusCode()
+ ", "
+ response.getStatusText());
}
});
} catch (IllegalArgumentException e) {
log.error(filePath + " download driver error", e);
throw DomainErrors.DOWNLOAD_DRIVER_ERROR.exception(e.getMessage());
}
}
}
public void delete(String databaseType) {
Path path = Paths.get(driverPath(databaseType));
try {
Files.deleteIfExists(path);
} catch (IOException e) {
log.error("delete driver error " + databaseType, e);
}
}
private String driverPath(String databaseType) {
String fileName = databaseType + ".jar";
String filePath;
if (driverBaseDirectory.endsWith(File.separator)) {
filePath = driverBaseDirectory + fileName;
} else {
filePath = driverBaseDirectory + File.separator + fileName;
}
return filePath;
}
}

View File

@@ -1,5 +0,0 @@
package com.databasir.core.infrastructure.meta;
public class DatabaseMetaResolver {
}