Mybatis支持GRPC protobuf

最近公司项目rpc使用Google grpc 作为rpc框架,数据传输DTO对象统一使用proto来定义,但由于DTO层的model和DAO层的model 在很大程度上是可以复用的,所以在DAO 层也想使用proto来定义,项目中ORM框架使用到了Mybatis框架,想要在Mybatis上支持grpc proto 需要处理那些工作呢?

Mapper Api 定义:

 int save(Promotion promotion);//注意这里定义的是具体Model类,但在Mapper.xml中parameterType则是Model$Builder类,目的用来获取插入数据时将数据库生成的自增ID返回,只所以是keyProperty 等于“id_” 是因为protobuf 生成的model属性进行了修改属性名称。

<insert id="save" parameterType="model.Promotion$Builder"
		keyProperty="id_" useGeneratedKeys="true">
		<![CDATA[
		INSERT INTO promotion 
		(name,xxx)
		VALUES
		(#{name}, ...)
		]]>
	</insert>

查询数据

  Promotion getById(int id);

<resultMap type="model.Promotion$Builder"
		id="PromotionResultMap">
		<id column="id" property="id" />
		<result column="name" property="name" />
        <!-- 省略更多属性... -->
	</resultMap>


<select id="getById" resultMap="PromotionResultMap">
    <![CDATA[
		SELECT * FROM promotion where id=#{id}
	]]>
</select>

由于原生Mybatis 本身不支持proto api的方式,所以为了让Mybatis可以灵活的支持proto,所以我们需要开发proto plugin来实现。

在mybatis-config.xml配置中增加插件配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<!-- <setting name="logImpl" value="STDOUT_LOGGING" /> -->
		<setting name="mapUnderscoreToCamelCase" value="true" />
	</settings>
	<typeHandlers />
	<plugins>
        <!-- Mybatis插件拦截器,用来对Mybatis执行结果进行动态修改处理 -->
		<plugin interceptor="mybatis.plugins.ProtobufInterceptor" />
	</plugins>
</configuration>
import java.sql.Statement;
import java.util.List;
import java.util.Properties;
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import com.google.common.collect.Lists;
import com.google.protobuf.GeneratedMessageV3;

// Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets",
    args = {Statement.class}),})
public class ProtobufInterceptor implements Interceptor {

  public Object intercept(Invocation invocation) throws Throwable {
    DefaultResultSetHandler value = (DefaultResultSetHandler) invocation.getTarget();
    Object result = invocation.getMethod().invoke(value, invocation.getArgs());
    if (result != null) {
      if (List.class.isAssignableFrom(result.getClass())) {
        List<?> list = (List<?>) result;
        return this.process(list);
      }
    }
    return result;
  }

  private List<?> process(List<?> list) {
    if (list.size() > 0) {
      if (GeneratedMessageV3.Builder.class.isAssignableFrom(list.get(0).getClass())) {
        List<Object> resultList = Lists.newArrayList();
        for (Object val : list) {
          @SuppressWarnings("rawtypes")
          Object rtnVal = ((GeneratedMessageV3.Builder) val).build();
          resultList.add(rtnVal);
        }
        return resultList;
      }
    }
    return list;
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {}

}

有时候我们数据库的数据类型和proto中定义的数据类型不一致的,我们需要转换处理,例如:枚举类型

当数据插入数据到DB时,需要将Enum类型转换成数字类型存储到DB中,在查询时又需要将数字转到到枚举类型

这时候就需要自定义Mybatis提供的BaseTypeHandler 来实现自定义类型解析处理了。

例如: 自定义枚举类型

package hander;

import java.lang.reflect.Method;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.protobuf.ProtocolMessageEnum;

/**
 * 通用枚举转换处理
 * 
 * @author KEVIN LUAN
 *
 */
@MappedJdbcTypes(value = {JdbcType.TINYINT})
@MappedTypes({ProtocolMessageEnum.class})
public class EnumHander extends BaseTypeHandler<ProtocolMessageEnum> {
  public Class<?> type;
  private Method method;
  private static final Logger LOGGER = LoggerFactory.getLogger(EnumHander.class);

  public EnumHander(Class<?> type) throws NoSuchMethodException, SecurityException {
    this.type = type;
    try {
      this.method = type.getMethod("forNumber", int.class);
    } catch (Exception e) {
      LOGGER.error("type:`" + type + "` getMethod:`forNumber` ERROR", e);
    }
  }

  public EnumHander() {}

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, ProtocolMessageEnum parameter,
      JdbcType jdbcType) throws SQLException {
    ps.setInt(i, parameter.getNumber());
  }

  private ProtocolMessageEnum forNumber(int value) {
    try {
      return (ProtocolMessageEnum) method.invoke(null, value);
    } catch (Exception e) {
      LOGGER.error("type:`" + type + "`.forNumber() ERROR", e);
      throw new RuntimeException("type:`" + type + "`.forNumber() ERROR", e);
    }
  }

  @Override
  public ProtocolMessageEnum getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    int value = rs.getInt(columnName);
    return forNumber(value);
  }

  @Override
  public ProtocolMessageEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    int value = rs.getInt(columnIndex);
    return forNumber(value);
  }

  @Override
  public ProtocolMessageEnum getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    int value = cs.getInt(columnIndex);
    return forNumber(value);
  }

}

自定义DateTime类型处理

package hander;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

/**
 * 数据类型转换从:代码中Long类型转到到Mysql中的datetime类型 pojo.Long->mysql.DateTime
 * 
 * @author KEVIN LUAN
 *
 */
@MappedJdbcTypes(value = {JdbcType.TIMESTAMP})
@MappedTypes({Long.class, long.class})
public class DateTimeHander extends BaseTypeHandler<Long> {
  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setTimestamp(i, new Timestamp(parameter));
  }

  @Override
  public Long getNullableResult(ResultSet rs, String columnName) throws SQLException {
    Timestamp timestamp = rs.getTimestamp(columnName);
    if (timestamp != null) {
      return timestamp.getTime();
    } else {
      return 0L;
    }
  }

  @Override
  public Long getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    Timestamp timestamp = rs.getTimestamp(columnIndex);
    if (timestamp != null) {
      return timestamp.getTime();
    } else {
      return 0L;
    }
  }

  @Override
  public Long getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    Timestamp timestamp = cs.getTimestamp(columnIndex);
    if (timestamp != null) {
      return timestamp.getTime();
    } else {
      return 0L;
    }
  }

}

对象JSON 序列化类型

package hander;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import com.google.protobuf.GeneratedMessageV3;

/**
 * 通用Proto Message转换处理
 * 
 * @author KEVIN LUAN
 *
 */
@MappedJdbcTypes(value = {JdbcType.TINYINT})
@MappedTypes({GeneratedMessageV3.class})
public class JsonHander extends BaseTypeHandler<GeneratedMessageV3> {
  public Class<?> type;

  public JsonHander(Class<?> type) throws NoSuchMethodException, SecurityException {
    this.type = type;
  }

  public JsonHander() {}

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, GeneratedMessageV3 parameter,
      JdbcType jdbcType) throws SQLException {
    if (parameter != null) {
      String json = JacksonSerialize.INSTANCE.encode(parameter);
      ps.setString(i, json);
    } else {
      ps.setString(i, null);
    }
  }

  private GeneratedMessageV3 jsonAsBean(String value) {
    if (StringUtils.isNotBlank(value)) {
      return (GeneratedMessageV3) JacksonSerialize.INSTANCE.decode(value, type);
    }
    return null;
  }

  @Override
  public GeneratedMessageV3 getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String value = rs.getString(columnName);
    return jsonAsBean(value);
  }

  @Override
  public GeneratedMessageV3 getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String value = rs.getString(columnIndex);
    return jsonAsBean(value);
  }

  @Override
  public GeneratedMessageV3 getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    String value = cs.getString(columnIndex);
    return jsonAsBean(value);
  }

}

在查询数据时使用ResultMap对属性指定对应的Hander即可例如

 <resultMap type="model.XXXUser$Builder"
               id="UserResultMap">
        <id column="id" property="id"/>
        <!-- 查询数据时会根据数据库中的DateTime类型自动转到long类型中 -->
        <result column="create_time" property="createTime" typeHandler="hander.DateTimeHander"/>
        <!-- 省略更多属性... -->
    </resultMap>


  <insert id="saveBatch" parameterType="model.User$Builder" keyProperty="id_"
            useGeneratedKeys="true">
        <![CDATA[
	INSERT INTO user 
		(xx)
		VALUES
	]]>
        <!-- 插入数据是将long类型转到DateTime类型 -->
       (#{item.createTime,typeHandler=hander.DateTimeHander})
    </insert>

<select id="get" resultMap="UserResultMap">
	<![CDATA[
		SELECT * FROM user limit 1;
    ]]>
</select>

到此 mybatis 支持proto就完成了。

猜你喜欢

转载自blog.csdn.net/kevin_Luan/article/details/81176510