说明:Java中,注解里的属性值在编译时就已经固定了,是无法通过AOP或者反射技术直接去修改的。本文介绍如何通过动态代理的方式来修改属性值。
搭建环境
首先,创建一个简单的Spring Boot项目,pom.xml文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/>
</parent>
<groupId>com.hezy</groupId>
<artifactId>tk_mapper_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Archetype - tk_mapper_demo</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
场景描述
如下,是一个实体类,属性limit
因为和MySQL的关键字冲突,所以用@Column注解做了转义
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Table(name = "i_users")
@Data
public class User implements Serializable {
@Id
private Integer id;
private String username;
private String password;
@Column(name = "`limit`")
private String limit;
}
数据访问层如下,继承框架自带的Mapper,可以使用它的一些API
import com.hezy.pojo.User;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;
@Repository
public interface UserRepository extends Mapper<User> {
}
接口访问数据库,直接使用其对应的API,如下:
@GetMapping("/hello/{id}")
public User hello(@PathVariable Long id) {
return userRepository.selectByPrimaryKey(id);
}
能成功访问,没得问题
现在问题来了,这个实体类是与数据库技术深度绑定的。实体类User的limit
属性名的转义方式是MySQL的,如果数据库换成了postgres,而postgres对关键字的转义使用双引号的,这样就会报错。如下:
(去掉mysql,换成postgresql)
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.mysql</groupId>-->
<!-- <artifactId>mysql-connector-j</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- </dependency>-->
访问接口,报语法错误
当然,会有同学问,一个项目不可能用两种数据库,但是如果你们公司是做SaaS应用的,要面对不同行业的客户,需要同时支持MySQL、Postgres,要在这两种数据库之间切换使用,那么这个问题是很棘手的。
博主自己想了几种办法,如下,
可行方法,是技术上可行,但不一定是好的。因为要考虑代码侵入、发布上线等一些场外因素。
动态代理
场景介绍完了,理一下。两种数据库对关键字的转义不同,且无法兼容,会直接报错。而转义字符是直接写在实体类属性上的,通过@Column注解,最后注解里面的name值是在编译时就固定了的,运行是无法改动,也就是说通过AOP去根据数据库类型(DBType)拼接不同的转义符是行不通的,无法改,改了也没法生效。
一筹莫展之际,我牛逼的同事给了我一个工具类,里面可以通过动态代理的方式,获取到JDK底层对象memberValues并修改。可以改就好办了,可以在项目启动时,主动走这个逻辑,把项目中相关实体类字段都改掉。
@PostConstruct
public void init() throws NoSuchFieldException, IllegalAccessException {
// 动态代理,获取实体类指定属性的上的注解
InvocationHandler invocationHandler = Proxy.getInvocationHandler(User.class
.getDeclaredField("limit")
.getAnnotation(Column.class));
// 反射获取memberValues
Field f = invocationHandler.getClass().getDeclaredField("memberValues");
f.setAccessible(true);
Map<String, Object> memberValues = (Map<String, Object>) f.get(invocationHandler);
// 修改注解的name值,把MySQL的转义方式换成postgresql的
memberValues.put("name", "\"limit\"");
}
改完之后,重新访问,如下,可以看到能正常访问数据库了
总结
本文介绍了一种修改注解里面属性值的方式,非常有启发,感觉自己功力大增。但这种方式,AI不推荐在生产环境中使用,理由有下:
-
涉及JDK内部实现,可能会破坏代码的可维护性和稳定性;
-
如上,关联到JDK内部实现细节,会有可移植性问题;