Mybatis支持GRPC protobuf

本文介绍如何通过开发插件及自定义类型处理器让Mybatis支持Google的Proto数据传输格式。

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

帮我看看这pom,有没有数据库相关的依赖 <?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <artifactId>video-analysis-algorithm</artifactId> <version>1.10.4</version> <name>video-analysis-algorithm</name> <description>Microservice for Video Analysis Algorithm</description> <properties> <maven.test.jvmargs>-Dfile.encoding=UTF-8</maven.test.jvmargs> <log4j2.version>2.20.0</log4j2.version> <micrometer.version>1.11.5</micrometer.version> <protobuf.version>3.23.4</protobuf.version> <protobuf-plugin.version>0.6.1</protobuf-plugin.version> <grpc.version>1.58.0</grpc.version> <grpc.starter.version>2.15.0.RELEASE</grpc.starter.version> <google.guava.version>31.1-jre</google.guava.version> <apache.skywalking.version>8.12.0</apache.skywalking.version> <nbu.skywalking-sdk.version>1.0.0</nbu.skywalking-sdk.version> <nbu.common.version>2.1.94</nbu.common.version> <nbu.common-api-proxy.version>1.0.98</nbu.common-api-proxy.version> <netty.version>4.1.108.Final</netty.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>com.vaadin.external.google</groupId> <artifactId>android-json</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.awaitility</groupId> <artifactId>awaitility</artifactId> <version>4.2.1</version> <scope>test</scope> </dependency> <!-- google guava--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${google.guava.version}</version> </dependency> <!-- grpc --> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-all</artifactId> <version>${grpc.version}</version> </dependency> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>${protobuf.version}</version> </dependency> <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-testing</artifactId> <version>${grpc.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>net.devh</groupId> <artifactId>grpc-spring-boot-starter</artifactId> <version>${grpc.starter.version}</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <!-- Skywalking --> <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-log4j-2.x</artifactId> <version>${apache.skywalking.version}</version> </dependency> <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-trace</artifactId> <version>${apache.skywalking.version}</version> </dependency> <dependency> <groupId>com.tplink.nbu</groupId> <artifactId>skywalking-sdk</artifactId> <version>${nbu.skywalking-sdk.version}</version> </dependency> <!-- nbu common--> <dependency> <groupId>com.tplink.nbu.common</groupId> <artifactId>pii-mask-spring-boot-autoconfigure</artifactId> <version>${nbu.common.version}</version> </dependency> <dependency> <groupId>com.tplink.nbu.common</groupId> <artifactId>common_api_proxy</artifactId> <version>${nbu.common-api-proxy.version}</version> </dependency> <dependency> <groupId>com.tplink</groupId> <artifactId>account-api-sdk</artifactId> <version>1.3.718</version> </dependency> <!-- https://mvnrepository.com/artifact/com.ibm.icu/icu4j --> <dependency> <groupId>com.ibm.icu</groupId> <artifactId>icu4j</artifactId> <version>77.1</version> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> <version>1.5.6</version> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>ffmpeg-platform</artifactId> <version>4.4-1.5.6</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <extensions> <extension> <groupId>kr.motd.maven</groupId> <artifactId>os-maven-plugin</artifactId> <version>1.5.0.Final</version> </extension> <extension> <groupId>org.kuali.maven.wagons</groupId> <artifactId>maven-s3-wagon</artifactId> <version>1.2.1</version> </extension> </extensions> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <excludes> <exclude>config/**</exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.0</version> <configuration> <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact> <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact> <pluginId>grpc-java</pluginId> <protoSourceRoot>src/main/proto</protoSourceRoot> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
最新发布
11-07
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值