文章目录
- 简介
- 1.依赖及配置文件
- 2.功能实现代码
-
- 2.1.自定义字段类型注解
- 2.2.添加适配器
-
- 2.2.1.定义适配器抽象类
- 2.2.2.数据库适配器实现类
- 2.3.添加测试用得模型类
- 2.4.配置需要通过反射修改字段类型类文件路径
- 2.5.通过反射修改字段对应的数据库字段类型 (核心代码)
- 2.6.在数据库连接源加载之前调用2.5的工具类
- 3.测试
-
- 3.1.测试数据库字段适配
- 3.2.测试数据库函数适配
- 4.项目配套代码下载
简介
因项目需求需要在应用编译发布后不改源代码的方式下支持MySql,Oracle等国产数据库,
因各数据库厂商的sql函数,字段类型,主键自增策略有差异,故本人基于jdk反射机制+适配器模式实现该需求。
本文以MySql,Oracle为例,下表列举了一些简单的差异信息
数据库 | 文本块类型 | 日期格式化函数 | 主键自增 |
---|---|---|---|
mysql | text | date_format() | 支持GenerationType.IDENTITY |
oracle | clob | to_char() | 不支持GenerationType.IDENTITY,支持GenerationType.SEQUENCE |
jpa默认String类型映射到数据库是字符串(varchar)类型,如果需要存储文本块字段,需要通过@Column注解的columnDefinition属性指定类型为text或者clob,如果项目默认在mysql环境开发的则切换到oracle环境则需要修改注解里的属性值,这种改动特别低效,我们可以通过Jdk反射加自定义注解解决这个问题。
@Column(columnDefinition="text")privateString likeBookList;
1.依赖及配置文件
pom文件如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--mysql jdbc连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--oracle jdbc连接驱动 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>12.2.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
application.yml文件如下
下面通过spring.profiles.active属性选择使用mysql或者oracle数据库#------------- 公共的配置属性 ---------------spring:profiles:#使用mysql或oracle改下面属性即可active: mysqldatasource:type: com.zaxxer.hikari.HikariDataSourcehikari:# 连接池名称pool-name: MyHikariCP#最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-sizeminimum-idle:10#连接池最大连接数,默认是10 (cpu核数量 * 2 + 硬盘数量)maximum-pool-size:30#空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。idle-timeout:600000#连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短max-lifetime:1800000#连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒connection-timeout:30000jpa:show-sql:truehibernate:naming:physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImplddl-auto: updateproperties:hibernate:jdbc:enable_lazy_load_no_trans:true#------------- mysql 配置 ------------------spring:config:activate:on-profile: mysqldatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/db_adapter?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=GMT%2b8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=15000&autoReconnect=true&cachePrepStmts=true&useServerPrepStmts=trueusername: rootpassword:123456hikari:#用于测试连接是否可用的查询语句connection-test-query: SELECT 1jpa:database: mysqlhibernate:properties:hibernate:jdbc:#配置hibernate方言使用Mysqldialect: org.hibernate.dialect.MySQL5InnoDBDialect#------------- oracle 配置 ------------------spring:config:activate:on-profile: oracledatasource:driver-class-name: oracle.jdbc.driver.OracleDriverurl: jdbc:oracle:thin:@localhost:1521:ORCLusername: DBADAPTERpassword:123456hikari:#用于测试连接是否可用的查询语句connection-test-query: SELECT * from dualjpa:database: oracleproperties:hibernate:jdbc:#配置hibernate方言使用Oracledialect: org.hibernate.dialect.OracleDialect#------------- 可以再扩展支持hibernate方言的数据库 ---------------
2.功能实现代码
2.1.自定义字段类型注解
当使用oracle数据库的时候,使用注解内的value属性替换类中的@Column注解columnDefinition属性,使用注解内strategy替换@GeneratedValue注解里的strategy属性。
/**
* @Author Dominick Li
* @CreateTime 2022/3/11 10:57
**/@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public@interfaceOracleCloumnDefinition{/**
* 数据库字段类型
*/Stringvalue()default"";/**
* 主键自增策略
*/GenerationTypestrategy()defaultGenerationType.SEQUENCE;}
2.2.添加适配器
枚举出应用支持的数据库
/**
* @Description 目前支持的数据库类型
* @Author Dominick Li
* @CreateTime 2022/3/9 17:15
**/publicenumDataBaseType{MYSQL("mysql"),ORACLE("oracle"),;privateString name;DataBaseType(String name){this.name= name;}publicstaticDataBaseTypenameOf(String name){for(DataBaseType dataBaseType:DataBaseType.values()){if(dataBaseType.name.equals(name)){return dataBaseType;}}returnnull;}}
2.2.1.定义适配器抽象类
@Slf4jpublicabstractclassDataBaseNativeSql{@Value("${spring.profiles.active}")privateString activeDatabase;privatestaticDataBaseType dataBaseType;publicstaticDataBaseTypegetDataBaseType(){return dataBaseType;}/**
* 把日期字段格式化成只包含年的字符 例如:2022-12-24 10:20 返回2022
*/publicabstractStringformat_year();/**
* 把日期字段格式化成只包含年月的字符 例如:2022-12-24 10:20 返回2022-12
*/publicabstractStringformat_year_month();/**
* 把日期字段格式化成只包含年月日的字符 例如:2022-12-24 10:20 返回2022-12-24
*/publicabstractStringformat_year_month_day();@PostConstructpublicvoidsupportsAdvice(){
log.info("当前系统使用的数据库是{}", activeDatabase);
dataBaseType=DataBaseType.nameOf(activeDatabase);if(dataBaseType==null){
log.error("未适配的数据库类型:{} ,系统异常退出!", activeDatabase);thrownewRuntimeException("未适配的数据库类型,系统异常退出!");}}}
2.2.2.数据库适配器实现类
下面通过@ConditionalOnProperty配置只由当配置文件中的spring.profiles.active属性和当前注解里havingValue 值一致的时候,当前类才由Spring ioc管理,故DataBaseNativeSql抽象类的实例永远只由一个存在。
@Configuration@ConditionalOnProperty(name="spring.profiles.active", havingValue="mysql")publicclassMysqlNativeSqlAdaptationextendsDataBaseNativeSql{@OverridepublicStringformat_year(){return"date_format(${field},'%Y')";}@OverridepublicStringformat_year_month(){return"date_format(${field},'%Y-%m')";}@OverridepublicStringformat_year_month_day(){return"date_format(${field},'%Y-%m-%d')";}}@Configuration@ConditionalOnProperty(name="spring.profiles.active", havingValue="oracle")publicclassOracleNativeSqlAdaptationextendsDataBaseNativeSql{@OverridepublicStringformat_year(){return"to_char(${field},'yyyy')";}@OverridepublicStringformat_year_month(){return"to_char(${field},'yyyy-mm')";}@OverridepublicStringformat_year_month_day(){return"to_char(${field},'yyyy-mm-dd')";}}
2.3.添加测试用得模型类
默认使用的mysql的字段类型,下面使用到OracleCloumnDefinition注解来支持动态修改字段类型
**/@Data@Entity@Table(name="sys_user")publicclassSysUser{/**
* 主键 自增策略 oracle=GenerationType.SEQUENCE, mysql=GenerationType.IDENTITY, 如果Id使用雪花算法生成或者UUID则可设置为默认值GenerationType.AUTO
* mysql数据库只有int类型支持主键自增,Long类型默认对应的Mysql数据库的bigint类型不支持自增
*/@Id@GeneratedValue(strategy=GenerationType.IDENTITY)@Column(columnDefinition="int")@OracleCloumnDefinition("number")privateLong id;/**
* 用户名
*/privateString username;/**
* 喜欢看的书 字段类型设置,默认用Mysql数据库配置为文本块text类型存储,oracle的文本块用clob存储
* 如果需要扩展其它的数据库,可根据Column配置的columnDefinition类型是否兼容,不兼容需要自定义扩展和@OracleCloumnDefinition类似的注解
*/@OracleCloumnDefinition("clob")@Column(columnDefinition="text")privateString likeBookList;/**
* 创建时间
*/privateDate createTime;/**
* 是否可用
* mysql中Column默认使用的bit类型存储boolean类型的值,oracle默认不支持boolean,需要动态修改columnDefinition成number类型
*/@OracleCloumnDefinition("number")@Columnprivateboolean enabled;}publicinterfaceSysUserRepositoryextendsJpaRepository<SysUser,Integer>{List<SysUser>findAllByEnabled(boolean enabled);}
2.4.配置需要通过反射修改字段类型类文件路径
publicclassWriteClassNameToFileUtils{publicstaticvoidmain(String[] args)throwsException{List<String> packNameList=Arrays.asList("com.ljm.dbadapter.model");StringBuilder sb=newStringBuilder();for(String packageName: packNameList){String path= packageName.replaceAll("\\.","/");File dir=org.springframework.util.ResourceUtils.getFile("classpath:"+ path);for(File file: dir.listFiles()){if(file.isDirectory()){continue;}else{if(sb.length()!=0){
sb.append("\n");}String className= packageName+"."+ file.getName().replace(".class","");
sb.append(className);}}}System.out.println(sb.toString());String resourcePath=newFile("src/main/resources/").getAbsolutePath();File file=newFile(resourcePath+File.separator+"className.txt");FileOutputStream fileOutputStream=newFileOutputStream(file);
fileOutputStream.write(sb.toString().getBytes());
fileOutputStream.flush();
fileOutputStream.close();}}
执行完会在项目的resources目录下生成这个文件
文件内容如下
com.ljm.dbadapter.model.SysUser
2.5.通过反射修改字段对应的数据库字段类型 (核心代码)
@Slf4jpublicclassColumnDefinitionAdaptaion{publicvoidinit(){try{//只有oracle需要对字段特殊处理,其它的按照默认类型即可,如需要扩充其它数据库加上 || 判断条件即可if(DataBaseNativeSql.getDataBaseType()==DataBaseType.ORACLE){
log.info("*****************************对实体类字段进行自动适配开始******************************");//第一步 加载需要扫描的class文件List<Class<?>> classList=getClassList();if(classList==null){return;}//第二步 通过反射机制修改类的Cloumn的columnDefinition属性for(Class<?> clazz: classList){modifyCloumnDefinition(clazz);}
log.info("*****************************对实体类字段进行自动适配结束******************************");}}catch(Exception e){
e.printStackTrace();
log.error("ColumnDefinitionAdaptaion error:{}", e.getMessage());}}/**
* 读取className文件获取需要加载的class文件
*/privateList<Class<?>>getClassList(){try(InputStreamReader isr=newInputStreamReader(getClass().getResourceAsStream("/className.txt"),"UTF-8")){List<Class<?>> classList=newArrayList<>();Class<?> beanClass;BufferedReader br=newBufferedReader(isr);String className="";while((className= br.readLine())!=null){
beanClass=Class.forName(className);//只加载包含@Table注解的类if(beanClass.isAnnotationPresent(Table.class)){
classList.add(beanClass);}}return classList;}catch(Exception e){
log.error("getClass error:{}", e.getMessage());returnnull;}}/**
* 修改注解里的字段类型
*/publicstaticvoidmodifyCloumnDefinition(Class<?> clas)throwsException{DataBaseType dataBaseType=DataBaseNativeSql.getDataBaseType();Column column;GeneratedValue generatedValue;OracleCloumnDefinition oracleCloumnDefinition;Map generatedValueMemberValues;boolean modify;InvocationHandler invocationHandler;Field hField;Field[] fields= clas.getDeclaredFields();for(Field field: fields){
generatedValueMemberValues=null;
modify=false;if(field.isAnnotationPresent(Column.class)){if(dataBaseType==DataBaseType.ORACLE&& field.isAnnotationPresent(OracleCloumnDefinition.class)){
modify=true;}elseif(true){//可以在if逻辑处扩展其它数据库判断逻辑}}if(modify){
column= field.getAnnotation(Column.class);// 获取column这个代理实例所持有的 InvocationHandler
invocationHandler=Proxy.getInvocationHandler(column);// 获取 AnnotationInvocationHandler 的 memberValues 字段
hField= invocationHandler.getClass().getDeclaredField("memberValues");// 因为这个字段事 private修饰,所以要打开访问权限
hField.setAccessible(true);// 获取 memberValuesMap memberValues=(Map) hField.get(invocationHandler);//判断是否为主键并设置了自增策略if(field.isAnnotationPresent(Id.class)&& field.isAnnotationPresent(GeneratedValue.class)){//修改自增策略
generatedValue= field.getAnnotation(GeneratedValue.class);
invocationHandler=Proxy.getInvocationHandler(generatedValue);
hField= invocationHandler.getClass().getDeclaredField("memberValues");
hField.setAccessible(true);
generatedValueMemberValues=(Map) hField.get(invocationHandler);}// 修改 value 属性值if(dataBaseType==DataBaseType.ORACLE){
oracleCloumnDefinition= field.getAnnotation(OracleCloumnDefinition.class);
memberValues.put("columnDefinition", oracleCloumnDefinition.value());if(generatedValueMemberValues!=
声明:本站所有文章,如无特殊说明或标注,均为网络收集发布。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。