了解了注解和AOP相关概念与技术实现之后,我们可以结合两者,做一些事情,如上文所说:
1.自定义简化版orm框架;
2.自定义简化版日志框架;
3.参数校验、过滤等。
这里,模拟orm框架。
整体思路:
自定义注解,在实体类上标注,告诉程序其所对应的表,对应的字段;在接口上标注,告诉程序该做什么操作(增删改查)。接口方法使用动态代理,解析方法上的注解,判断所要做的操作,根据操作不同,解析成对应的sql。解析sql,用到实体类上的注解,并结合上一步中方法上的注解。根据sql,执行对应的数据库操作。
大致设计:
在实体类上,我们需要告知程序对应的表、字段,那么就需要两个注解:@Table、@Column,同时,需要知道查询条件,那么还需要一个注解:@Query;在接口上,我们可以定义一个基础的接口,用来结合实体类上的注解,实现简单的增删改查,那么我们需要一个注解,来告知对应操作:@Operation;有时候,我们需要自己写sql,这个可以标注在方法上:@Sql。对于查询条件,可以放在实体类中,作为参数传入接口方法。
实现步骤:
1.自定义注解
Table 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
Column 注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String value();
}
Query 注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
String value() default "=";
String expr() default "%-%";// 查询条件默认%-%匹配中间,%- 匹配结尾,-%匹配开头
}
注:查询条件拼接,默认 = ,可自行设置 =、>、<。设置为 like时,可设置匹配方式 expr。
Operation 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Operation {
OperationType value();
}
注:在Operation注解中,操作类型的值,使用枚举。
OperationType 枚举
public enum OperationType {
SELECT,
INSERT,
UPDATE,
DELETE
}
Sql 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sql {
String value();
String[] params() default {};
}
注:value存储sql语句。若需要参数,语句中用?占位,params中存方法入参类的属性名,顺序对应sql中的?顺序。若不需要参数,则无需设置params。
2.实体类、接口标注注解
User.java 实体类标注
@Table("user")
public class User {
@Query
@Column("id")
private int id;
// @Query("like")
@Column("name")
private String name;
@Column("phone")
private String phone;
private String address;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
BaseDao.java 基础接口标注
/**
* 四个基础的语句,查询条件为实体类中所设置的Query
*
*/
public interface BaseDao {
@Operation(OperationType.SELECT)
List selByEntity(T t);
@Operation(OperationType.INSERT)
int addEntity(T t);
@Operation(OperationType.UPDATE)
int updateEntity(T t);
@Operation(OperationType.DELETE)
int delEntity(T t);
}
注:基础接口使用泛型,传入类型参数。增加、更新、删除操作,返回影响记录数。
UserDao.java 接口标注
public interface UserDao extends BaseDao{
@Sql(value = "delete from user where id = ?",params = {"id"})
void delUser(User user);
@Sql(value = "insert into user (id,name,phone) values (?,'?','?')",params = {"id","name","phone"})
void addUser(User user);
@Sql(value = "update user set name = '?',phone = '?' where id = '?'",params = {"name","phone","id"})
void updateUser(User user);
@Sql(value = "select id,name,phone from user where name like '%?%'",params = {"name"})
List selUser(User user);
// 测试无参数sql
@Sql("select id,name,phone from user where name like '3%'")
List selUser3();
@Sql( "delete from user where id = 999")
void delUser3();
}
3.动态代理提供组装、执行sql的场地
这里使用动态代理,为接口生成代理类,从而使接口可进行调用。在代理方法中,获取并解析原接口方法上的注解,从而做出相应的动作。
MapperProxy.java 动态代理类
public class MapperProxy {
public static T getProxyInstance(Class mapperClass){
Object obj = null;
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj ;
if (args != null){
// 基础接口方法中传入的参数,默认为一个对应的实体类
obj = args[0];
}else {
// 如果方法没有入参,则去接口中寻找
// 获取接口中传入的实体类,并通过反射生成对象
Type[] type = mapperClass.getGenericInterfaces();
ParameterizedType parameterizedType = (ParameterizedType) type[0];
Type[] types = parameterizedType.getActualTypeArguments();
Class clazz = Class.forName(types[0].getTypeName());
obj = clazz.newInstance();
}
// result 为代理方法的返回结果,可将select语句的返回结果放入这里。其它语句返回 更新的记录数。
Object result = null;
try {
DBUtil dbUtil = new DBUtil();
// 这里拦截了所有方法。
// 获取方法上的注解,以此判断所要进行的操作。
Operation operation = method.getAnnotation(Operation.class);
if (operation != null){
if(operation.value() == OperationType.SELECT){
result = dbUtil.selByEntity(obj);
}else if(operation.value() == OperationType.UPDATE){
result = dbUtil.updateEntity(obj);
}else if(operation.value() == OperationType.INSERT){
result = dbUtil.addEntity(obj);
}else if(operation.value() == OperationType.DELETE){
result = dbUtil.delEntity(obj);
}
}
// 这里是自定义sql语句的方法
Sql sql = method.getAnnotation(Sql.class);
if (sql != null){
if (sql.value().startsWith("select")){
result = dbUtil.sqlExecuteSelect(obj,sql);
}else {
result = dbUtil.sqlExecute(obj,sql);
}
}
}catch (Exception e){
e.printStackTrace();
}
return result;
}
};
return (T)Proxy.newProxyInstance(MapperProxy.class.getClassLoader(), new Class[]{mapperClass}, handler);
}
}
注:getProxyInstance() 方法 接收泛型参数,从而返回对应dao类型。
4.工具类组装、执行sql
SqlUtil.java 组装sql
public class SqlUtil {
public static String selectSQL(Object obj) throws Exception{
StringBuffer sql = new StringBuffer();
sql.append("select ");
// 获取类上的注解
Table table = obj.getClass().getAnnotation(Table.class);
if (table != null) {
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field:fields){
// 获取属性上的注解
Column column = field.getAnnotation(Column.class);
if(column != null){
sql.append(column.value()+",");
}
}
sql.deleteCharAt(sql.length() - 1);
sql.append(" from " + table.value());
sql.append(sqlWhere(obj));
}
return sql.toString();
}
public static String insertSQL(Object obj) throws Exception{
StringBuffer sql1 = new StringBuffer();
sql1.append("insert into ");
StringBuffer sql2 = new StringBuffer();
sql2.append(" values (");
// 获取类上的注解
Table table = obj.getClass().getAnnotation(Table.class);
if (table != null){
sql1.append(table.value()+" ( ");
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field:fields){
Column column = field.getAnnotation(Column.class);
if(column != null){
sql1.append(column.value()+",");
field.setAccessible(true);
if(field.getGenericType().toString().equals("class java.lang.String")){
sql2.append("'" + field.get(obj)+"',");
}else {
sql2.append(field.get(obj)+",");
}
}
}
sql1.deleteCharAt(sql1.length() - 1);
sql1.append(") ");
sql2.deleteCharAt(sql2.length() - 1);
sql2.append(") ");
sql1.append(sql2);
}
return sql1.toString();
}
public static String updateSQL(Object obj) throws Exception{
StringBuffer sql = new StringBuffer();
sql.append("update ");
// 获取类上的注解
Table table = obj.getClass().getAnnotation(Table.class);
if (table != null){
sql.append(table.value()+" set ");
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field:fields) {
// 获取属性上的注解
Column column = field.getAnnotation(Column.class);
if (column != null) {
field.setAccessible(true);
if(field.getGenericType().toString().equals("class java.lang.String")){
// 若属性为String ,组装时则需要 ''
sql.append(column.value()+ "= '" + field.get(obj) + "' ,");
}else {
sql.append(column.value()+ "= " + field.get(obj) + " ,");
}
}
}
sql.deleteCharAt(sql.length() - 1);
// 组装 where
sql.append(sqlWhere(obj));
}
return sql.toString();
}
public static String deleteSQL(Object obj) throws Exception{
StringBuffer sql = new StringBuffer();
sql.append("delete from ");
Table table = obj.getClass().getAnnotation(Table.class);
if (table != null){
sql.append(table.value());
sql.append(sqlWhere(obj));
}
return sql.toString();
}
public static String sqlSQL(Object obj,Sql sql) throws Exception{
// 若 params 为空,则不需要拼接参数,直接返回Sql注解上的value值
String[] params = sql.params();
if (params.length == 0){
return sql.value();
}
// 1.将params中的属性名替换成值
Object[] pars = new Object[params.length];
for (int i = 0;i
DBUtil.java 执行sql
public class DBUtil {
private static String driverName = "com.mysql.cj.jdbc.Driver";
private static String url = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false";
private static String username = "root";
private static String password = "root";
/**
* 获取数据库链接
* @param driverName
* @param url
* @param username
* @param password
* @return
*/
public static Connection getConn(String driverName, String url, String username, String password) {
Connection conn = null;
try {
// 加载驱动
Class.forName(driverName);
conn = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 执行 update、insert、delete语句,返回影响记录数
* @param sql
* @return
* @throws Exception
*/
public int execute(String sql) throws Exception{
int count = 0;
Connection conn = null;
try {
conn = getConn(driverName, url, username, password);
// 设置事务不自动提交
conn.setAutoCommit(false);
// 回滚点
conn.setSavepoint();
PreparedStatement pst;
pst = conn.prepareStatement(sql);
count = pst.executeUpdate();
conn.commit();// 提交
}catch (Exception e){
e.printStackTrace();
}
return count;
}
/**
* 执行 select 语句,返回list
* @param obj
* @param sql
* @return
* @throws Exception
*/
public List
注:这里每次执行一次数据库操作,都会去连接一次数据库,非常不合理。后续文章中,对这个orm进行优化,增加数据库连接池。对于数据库信息,也做可配置化,放入db.properties中。
5.应用示例
public static void main(String[] args) {
/* */
User user = new User();
user.setId(1);
user.setName("nep");
user.setPhone("123456");
UserDao userDao = MapperProxy.getProxyInstance(UserDao.class);
Object obj = null;
// obj = userDao.addEntity(user);
// obj = userDao.updateEntity(user);
// obj = userDao.delEntity(user);
// obj = userDao.selByEntity(user);
System.out.println(JSON.toJSONString(obj));
}
至此,一个简易版的ORM完成了,对于注解结合AOP思想(这里用了动态代理),有了一定的实践。不过,这里仍有许多不足之处,如缺少数据库连接池、基础sql的拼接中参数不灵活、事务没有分开,无法进行多条sql的统一事务处理、缺少批量更新等,后续文章进行持续优化。
附录:
源码下载:
链接:https://pan.baidu.com/s/1B9kJWKP6QlmEYm-YR1B0fw
提取码:7z3a