最近公司项目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就完成了。