夿°æ®æº
å顾
éè¿åé¢æç« çä»ç»ï¼ç®åå·²ç»æ¯æä¸»æµæ°æ®åºï¼å æ¬MySqlï¼PostgreSqlï¼Oracleï¼Microsoft SQL Serverçï¼éè¿é ç½®é¶ä»£ç å®ç°äºCRUDå¢å æ¹æ¥RESTful APIãéç¨æ½è±¡å·¥å设计模å¼ï¼å¯ä»¥æ ç¼åæ¢ä¸åç±»åçæ°æ®åºã 使¯å¦æéè¦åæ¶æ¯æä¸åç±»åçæ°æ®åºï¼å¦ä½éè¿é ç½®è¿è¡ç®¡çå¢ï¼è¿æ¶åå¼å ¥å¤æ°æ®æºåè½å°±å¾æå¿ è¦äºã
ç®ä»
å©ç¨spring boot夿°æ®æºåè½ï¼å¯ä»¥åæ¶æ¯æä¸åç±»åæ°æ®åºmysqlï¼oracleï¼postsqlï¼sql serverçï¼ä»¥åç¸åç±»åæ°æ®åºä¸åçschemaãé¶ä»£ç åæ¶çæä¸åç±»åæ°æ®åºå¢å æ¹æ¥RESTful apiï¼ä¸æ¯æå䏿¥å£ä¸è·¨åºæ°æ®è®¿é®äºæ¬¡å¼åã
UIçé¢
é ç½®ä¸ä¸ªæ°æ®æºï¼å¤ä¸ªä»æ°æ®æºï¼æ¯ä¸ä¸ªæ°æ®æºç¸äºç¬ç«é ç½®å访é®ã
æ ¸å¿åç
é ç½®æ°æ®åºè¿æ¥ä¸²
é ç½®application.propertiesï¼spring.datasource为é»è®¤ä¸»æ°æ®æºï¼spring.datasource.hikari.data-sources[]æ°ç»ä¸ºä»æ°æ®æº
#primary
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/crudapi?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root
#postgresql
spring.datasource.hikari.data-sources[0].postgresql.driverClassName=org.postgresql.Driver
spring.datasource.hikari.data-sources[0].postgresql.url=jdbc:postgresql://localhost:5432/crudapi
spring.datasource.hikari.data-sources[0].postgresql.username=postgres
spring.datasource.hikari.data-sources[0].postgresql.password=postgres
#sqlserver
spring.datasource.hikari.data-sources[1].sqlserver.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.hikari.data-sources[1].sqlserver.url=jdbc:sqlserver://localhost:1433;SelectMethod=cursor;DatabaseName=crudapi
spring.datasource.hikari.data-sources[1].sqlserver.username=sa
spring.datasource.hikari.data-sources[1].sqlserver.password=Mssql1433
#oracle
spring.datasource.hikari.data-sources[2].oracle.url=jdbc:oracle:thin:@//localhost:1521/XEPDB1
spring.datasource.hikari.data-sources[2].oracle.driverClassName=oracle.jdbc.OracleDriver
spring.datasource.hikari.data-sources[2].oracle.username=crudapi
spring.datasource.hikari.data-sources[2].oracle.password=crudapi
#mysql
spring.datasource.hikari.data-sources[3].mysql.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.data-sources[3].mysql.url=jdbc:mysql://localhost:3306/crudapi2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.hikari.data-sources[3].mysql.username=root
spring.datasource.hikari.data-sources[3].mysql.password=root
å¨ææ°æ®æºââDynamicDataSource
Spring bootæä¾äºæ½è±¡ç±»AbstractRoutingDataSourceï¼å¤åæ¥å£determineCurrentLookupKeyï¼ å¯ä»¥å¨æ§è¡æ¥è¯¢ä¹åï¼è®¾ç½®ä½¿ç¨çæ°æ®æºï¼ä»èå®ç°å¨æåæ¢æ°æ®æºã
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
æ°æ®æºContextââDataSourceContextHolder
é»è®¤ä¸»æ°æ®æºå称为datasourceï¼ä»æ°æ®æºåç§°ä¿åå¨ThreadLocalåéCONTEXT_HOLDERéé¢ï¼ThreadLocalå«å线ç¨åé, æææ¯ThreadLocalä¸å¡«å çåéå±äºå½å线ç¨, 该åéå¯¹å ¶ä»çº¿ç¨èè¨æ¯é离ç, ä¹å°±æ¯è¯´è¯¥å鿝å½å线ç¨ç¬æçåéã
å¨RestControlleré颿 ¹æ®éè¦æå设置好å½åéè¦è®¿é®çæ°æ®æºkeyï¼å³è°ç¨setDataSourceæ¹æ³ï¼è®¿é®æ°æ®çæ¶åè°ç¨getDataSourceæ¹æ³è·åå°æ°æ®æºkeyï¼æç»ä¼ éç»DynamicDataSourceã
public class DataSourceContextHolder {
//é»è®¤æ°æ®æºprimary=dataSource
private static final String DEFAULT_DATASOURCE = "dataSource";
//ä¿å线ç¨è¿æ¥çæ°æ®æº
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> HEADER_HOLDER = new ThreadLocal<>();
public static String getDataSource() {
String dataSoure = CONTEXT_HOLDER.get();
if (dataSoure != null) {
return dataSoure;
} else {
return DEFAULT_DATASOURCE;
}
}
public static void setDataSource(String key) {
if ("primary".equals(key)) {
key = DEFAULT_DATASOURCE;
}
CONTEXT_HOLDER.set(key);
}
public static void cleanDataSource() {
CONTEXT_HOLDER.remove();
}
public static void setHeaderDataSource(String key) {
HEADER_HOLDER.set(key);
}
public static String getHeaderDataSource() {
String dataSoure = HEADER_HOLDER.get();
if (dataSoure != null) {
return dataSoure;
} else {
return DEFAULT_DATASOURCE;
}
}
}
å¨ææ°æ®åºæä¾è ââDynamicDataSourceProvider
ç¨åºå¯å¨æ¶åï¼è¯»åé ç½®æä»¶application.properties䏿°æ®æºä¿¡æ¯ï¼æå»ºDataSourceå¹¶éè¿æ¥å£setTargetDataSourcesè®¾ç½®ä»æ°æ®æºãæ°æ®æºçkeyåDataSourceContextHolderä¸keyä¸ä¸å¯¹åº
@Component
@EnableConfigurationProperties(DataSourceProperties.class)
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public class DynamicDataSourceProvider implements DataSourceProvider {
@Autowired
private DynamicDataSource dynamicDataSource;
private List<Map<String, DataSourceProperties>> dataSources;
private Map<Object,Object> targetDataSourcesMap;
@Resource
private DataSourceProperties dataSourceProperties;
private DataSource buildDataSource(DataSourceProperties prop) {
DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName(prop.getDriverClassName());
builder.username(prop.getUsername());
builder.password(prop.getPassword());
builder.url(prop.getUrl());
return builder.build();
}
@Override
public List<DataSource> provide() {
Map<Object,Object> targetDataSourcesMap = new HashMap<>();
List<DataSource> res = new ArrayList<>();
if (dataSources != null) {
dataSources.forEach(map -> {
Set<String> keys = map.keySet();
keys.forEach(key -> {
DataSourceProperties properties = map.get(key);
DataSource dataSource = buildDataSource(properties);
targetDataSourcesMap.put(key, dataSource);
});
});
//æ´æ°dynamicDataSource
this.targetDataSourcesMap = targetDataSourcesMap;
dynamicDataSource.setTargetDataSources(targetDataSourcesMap);
dynamicDataSource.afterPropertiesSet();
}
return res;
}
@PostConstruct
public void init() {
provide();
}
public List<Map<String, DataSourceProperties>> getDataSources() {
return dataSources;
}
public void setDataSources(List<Map<String, DataSourceProperties>> dataSources) {
this.dataSources = dataSources;
}
public List<Map<String, String>> getDataSourceNames() {
List<Map<String, String>> dataSourceNames = new ArrayList<Map<String, String>>();
Map<String, String> dataSourceNameMap = new HashMap<String, String>();
dataSourceNameMap.put("name", "primary");
dataSourceNameMap.put("caption", "ä¸»æ°æ®æº");
dataSourceNameMap.put("database", parseDatabaseName(dataSourceProperties));
dataSourceNames.add(dataSourceNameMap);
if (dataSources != null) {
dataSources.forEach(map -> {
Set<Map.Entry<String, DataSourceProperties>> entrySet = map.entrySet();
for (Map.Entry<String, DataSourceProperties> entry : entrySet) {
Map<String, String> t = new HashMap<String, String>();
t.put("name", entry.getKey());
t.put("caption", entry.getKey());
DataSourceProperties p = entry.getValue();
t.put("database", parseDatabaseName(p));
dataSourceNames.add(t);
}
});
}
return dataSourceNames;
}
public String getDatabaseName() {
List<Map<String, String>> dataSourceNames = this.getDataSourceNames();
String dataSource = DataSourceContextHolder.getDataSource();
Optional<Map<String, String>> op = dataSourceNames.stream()
.filter(t -> t.get("name").toString().equals(dataSource))
.findFirst();
if (op.isPresent()) {
return op.get().get("database");
} else {
return dataSourceNames.stream()
.filter(t -> t.get("name").toString().equals("primary"))
.findFirst().get().get("database");
}
}
private String parseDatabaseName(DataSourceProperties p) {
String url = p.getUrl();
String databaseName = "";
if (url.toLowerCase().indexOf("databasename") >= 0) {
String[] urlArr = p.getUrl().split(";");
for (String u : urlArr) {
if (u.toLowerCase().indexOf("databasename") >= 0) {
String[] uArr = u.split("=");
databaseName = uArr[uArr.length - 1];
}
}
} else {
String[] urlArr = p.getUrl().split("\\?")[0].split("/");
databaseName = urlArr[urlArr.length - 1];
}
return databaseName;
}
public Map<Object,Object> getTargetDataSourcesMap() {
return targetDataSourcesMap;
}
}
å¨ææ°æ®æºé ç½®ââDynamicDataSourceConfig
é¦å åæ¶ç³»ç»èªå¨æ°æ®åºé ç½®ï¼è®¾ç½®exclude = { DataSourceAutoConfiguration.class }
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class, args);
}
}
ç¶åèªå®ä¹Beanï¼åå«å®ä¹ä¸»æ°æ®æºdataSourceåå¨ææ°æ®æºdynamicDataSourceï¼å¹¶ä¸æ³¨å ¥å°JdbcTemplateï¼NamedParameterJdbcTemplateï¼åDataSourceTransactionManagerä¸ï¼å¨è®¿é®æ°æ®æ¶åèªå¨è¯å«å¯¹åºçæ°æ®æºã
//æ°æ®æºé
置类
@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DynamicDataSourceConfig {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceConfig.class);
@Resource
private DataSourceProperties dataSourceProperties;
@Bean(name = "dataSource")
public DataSource getDataSource(){
DataSourceBuilder<?> builder = DataSourceBuilder.create();
builder.driverClassName(dataSourceProperties.getDriverClassName());
builder.username(dataSourceProperties.getUsername());
builder.password(dataSourceProperties.getPassword());
builder.url(dataSourceProperties.getUrl());
return builder.build();
}
@Primary //å½ç¸åç±»åçå®ç°ç±»å卿¶ï¼éæ©è¯¥æ³¨è§£æ è®°çç±»
@Bean("dynamicDataSource")
public DynamicDataSource dynamicDataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//é»è®¤æ°æ®æº
dynamicDataSource.setDefaultTargetDataSource(getDataSource());
Map<Object,Object> targetDataSourcesMap = new HashMap<>();
dynamicDataSource.setTargetDataSources(targetDataSourcesMap);
return dynamicDataSource;
}
//äºå¡ç®¡çå¨DataSourceTransactionManageræé åæ°éè¦DataSource
//è¿éå¯ä»¥ç尿们ç»çæ¯dynamicDSè¿ä¸ªbean
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dynamicDataSource());
}
//è¿éçJdbcTemplateæé åæ°åæ ·éè¦ä¸ä¸ªDataSource,为äºå®ç°æ°æ®æºåæ¢æ¥è¯¢ï¼
//è¿é使ç¨ç乿¯dynamicDSè¿ä¸ªbean
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbc(){
return new JdbcTemplate(dynamicDataSource());
}
//è¿éçJdbcTemplateæé åæ°åæ ·éè¦ä¸ä¸ªDataSource,为äºå®ç°æ°æ®æºåæ¢æ¥è¯¢ï¼
//è¿é使ç¨ç乿¯dynamicDSè¿ä¸ªbean
@Bean(name = "namedParameterJdbcTemplate")
public NamedParameterJdbcTemplate getNamedJdbc(){
return new NamedParameterJdbcTemplate(dynamicDataSource());
}
}
请æ±å¤´è¿æ»¤å¨ââHeadFilter
æ¦æªææhttp请æ±ï¼ä»headeréé¢è§£æåºå½åéè¦è®¿é®çæ°æ®æºï¼ç¶å设置å°çº¿ç¨åéHEADER_HOLDERä¸ã
@WebFilter(filterName = "headFilter", urlPatterns = "/*")
public class HeadFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(HeadFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!"/api/auth/login".equals(request.getRequestURI())
&& !"/api/auth/jwt/login".equals(request.getRequestURI())
&& !"/api/auth/logout".equals(request.getRequestURI())
&& !"/api/metadata/dataSources".equals(request.getRequestURI())) {
String dataSource = request.getParameter("dataSource");
HeadRequestWrapper headRequestWrapper = new HeadRequestWrapper(request);
if (StringUtils.isEmpty(dataSource)) {
dataSource = headRequestWrapper.getHeader("dataSource");
if (StringUtils.isEmpty(dataSource)) {
dataSource = "primary";
headRequestWrapper.addHead("dataSource", dataSource);
}
}
DataSourceContextHolder.setHeaderDataSource(dataSource);
// finish
filterChain.doFilter(headRequestWrapper, response);
} else {
filterChain.doFilter(request, response);
}
}
}
å®é åºç¨
åé¢å¨ææ°æ®æºé ç½®åå¤å·¥ä½å·²ç»å®æï¼æåæä»¬å®ä¹åé¢DataSourceAspect
@Aspect
public class DataSourceAspect {
private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
@Pointcut("within(cn.crudapi.api.controller..*)")
public void applicationPackagePointcut() {
}
@Around("applicationPackagePointcut()")
public Object dataSourceAround(ProceedingJoinPoint joinPoint) throws Throwable {
String dataSource = DataSourceContextHolder.getHeaderDataSource();
DataSourceContextHolder.setDataSource(dataSource);
try {
return joinPoint.proceed();
} finally {
DataSourceContextHolder.cleanDataSource();
}
}
}
å¨API对åºçcontroller䏿¦æªï¼è·åå½åç请æ±å¤´æ°æ®æºkeyï¼ç¶åæ§è¡joinPoint.proceed()ï¼æå忢夿°æ®æºãå½ç¶å¨serviceå é¨è¿å¯ä»¥å¤æ¬¡åæ¢æ°æ®æºï¼åªéè¦è°ç¨DataSourceContextHolder.setDataSource()å³å¯ãæ¯å¦å¯ä»¥ä»mysqlæ°æ®åºè¯»åæ°æ®ï¼ç¶åä¿åå°oracleæ°æ®åºä¸ã
å端éæ
å¨è¯·æ±å¤´éé¢è®¾ç½®dataSource为对åºçæ°æ®æºï¼æ¯å¦primaryè¡¨ç¤ºä¸»æ°æ®æºï¼postgresqlè¡¨ç¤ºä»æ°æ®æºpostgresqlï¼å ·ä½å¯ä»¥åç§°åapplication.propertiesé ç½®ä¿æä¸è´ã
é¦å è°ç¨çå°æ¹é ç½®dataSource
const table = {
list: function(dataSource, tableName, page, rowsPerPage, search, query, filter) {
return axiosInstance.get("/api/business/" + tableName,
{
params: {
offset: (page - 1) * rowsPerPage,
limit: rowsPerPage,
search: search,
...query,
filter: filter
},
dataSource: dataSource
}
);
},
}
ç¶åå¨axioséé¢ç»ä¸æ¦æªé ç½®
axiosInstance.interceptors.request.use(
function(config) {
if (config.dataSource) {
console.log("config.dataSource = " + config.dataSource);
config.headers["dataSource"] = config.dataSource;
}
return config;
},
function(error) {
return Promise.reject(error);
}
);
ææå¦ä¸
å°ç»
æ¬æä¸»è¦ä»ç»äºå¤æ°æ®æºåè½ï¼å¨åä¸ä¸ªJavaç¨åºä¸ï¼éè¿å¤æ°æ®æºåè½ï¼ä¸éè¦ä¸è¡ä»£ç ï¼æä»¬å°±å¯ä»¥å¾å°ä¸åæ°æ®åºçåºæ¬crudåè½ï¼å æ¬APIåUIã
crudapiç®ä»
crudapiæ¯crud+apiç»åï¼è¡¨ç¤ºå¢å æ¹æ¥æ¥å£ï¼æ¯ä¸æ¬¾é¶ä»£ç å¯é ç½®ç产åã使ç¨crudapiå¯ä»¥å嫿¯ç¥æ å³çå¢å æ¹æ¥ä»£ç ï¼è®©æ¨æ´å 䏿³¨ä¸å¡ï¼èçº¦å¤§éææ¬ï¼ä»èæé«å·¥ä½æçã crudapiçç®æ æ¯è®©å¤çæ°æ®å徿´ç®åï¼ææäººé½å¯ä»¥å 费使ç¨ï¼ æ éç¼ç¨ï¼éè¿é ç½®èªå¨çæcrudå¢å æ¹æ¥RESTful APIï¼æä¾åå°UI管çä¸å¡æ°æ®ãåºäºä¸»æµç弿ºæ¡æ¶ï¼æ¥æèªä¸»ç¥è¯äº§æï¼æ¯æäºæ¬¡å¼åã
demoæ¼ç¤º
crudapiå±äºäº§å级çé¶ä»£ç å¹³å°ï¼ä¸åäºèªå¨ä»£ç çæå¨ï¼ä¸éè¦çæControllerãServiceãRepositoryãEntityçä¸å¡ä»£ç ï¼ç¨åºè¿è¡èµ·æ¥å°±å¯ä»¥ä½¿ç¨ï¼çæ£0代ç ï¼å¯ä»¥è¦çåºæ¬çåä¸å¡æ å ³çCRUD RESTful APIã
å®ç½å°åï¼crudapi.cn
æµè¯å°åï¼demo.crudapi.cn/crudapi/logâ¦
éæºç å°å
GitHubå°å
Giteeå°å
ç±äºç½ç»åå ï¼GitHubå¯è½éåº¦æ ¢ï¼æ¹æè®¿é®Giteeå³å¯ï¼ä»£ç åæ¥æ´æ°ã