基于Mybatis拦截器实现通用Mapper
- 定义通用mapper
- 定义拦截器
- 调用
在实际开发中,我们可能会有这样的需求,将业务SQL配置到数据库中,这样在生产环境下,如果我们业务SQL发生改变(查询字段不变),只需要将sql缓存刷新即可,无需修改mapper文件,重启服务器。
下面介绍我自己根据mybatis拦截器实现的方式:
定义通用mapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.oss.monitor.mapper.ExecuteMapper">
<select id="executeSelectSql" parameterType="hashmap" resultType="com.oss.monitor.base.jdbcParam.ResultType">
${sql}
</select>
</mapper>
注意:这里的resultType是一个自定义类型,方便我们后面拦截器拦截。
/**
* @program: monitor
* @description:只为了做通用返回类型标识作用
* @author: fuqiang
* @date: 2020-01-09 09:59
**/
public class ResultType {
}
@Mapper
public interface ExecuteMapper {
List<?> executeSelectSql(Map map);
}
定义拦截器
import com.oss.monitor.utils.CaseUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @program: hn_monitor
* @description:
* @author: fuqiang
* @date: 2020-01-13 09:16
**/
@Component
@Intercepts({ @Signature(type= Executor.class, method="query", args={MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
public class SetResultTypeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
//获取当前resutType的类型
Class<?> resultType = resultMaps.get(0).getType();
ResultSet resultSet = null;
PreparedStatement preparedStatement = null;
Connection connection = null;
try {
if(resultMapCount>0 && ("com.oss.monitor.base.jdbcParam.ResultType").equalsIgnoreCase(resultType.getName())){
//获取全局配置Configuration
Configuration configuration = mappedStatement.getConfiguration();
//获取BoundSQl
Object object = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(object);
//获取sql
String sql = boundSql.getSql();
//获取参数(取返回值类型)
Map parameterMap = (Map)boundSql.getParameterObject();
//如果参数中没有指定返回值类型,则默认返回Object
Class<?> resultTypeClass = Object.class;
if (parameterMap.containsKey("resultType")){
String resultTypeValue = (String)parameterMap.get("resultType");
resultTypeClass = Class.forName(resultTypeValue);
}
connection = configuration.getEnvironment().getDataSource().getConnection();
preparedStatement = connection.prepareStatement(sql);
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, object, boundSql);
parameterHandler.setParameters(preparedStatement);
//执行获得结果集
resultSet = preparedStatement.executeQuery();
List<Object> resList = new ArrayList<>();
if (resultSet != null) {
while (resultSet.next()) {
// 遍历一次是一行,也对应一个对象,利用反射new一个对象
Object result = resultTypeClass.newInstance();
// 要获取每一列的值,然后封装到结果对象中对应的属性名称上
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 0; i < columnCount; i++) {
// 获取每一列的值
Object value = resultSet.getObject(i + 1);
if (value instanceof BigDecimal){
value = Long.parseLong(value.toString());
}
// 列的名称
String columnName = metaData.getColumnName(i + 1);
String newColumnName = CaseUtil.underlineToCamel(columnName,'_');
// 列名和属性名称要严格一致
Field field = resultTypeClass.getDeclaredField(newColumnName);
field.setAccessible(true);
// 给映射的对象赋值
field.set(result, value);
}
resList.add(result);
}
return resList;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return invocation.proceed();
}
public Object plugin(Object target) {
// 读取@Signature中的配置,判断是否需要生成代理类
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
说明:mybatis拦截器只能去拦截四大组件,这边拦截的是Executor。执行到这一步,sql解析的工作已经完成,所以我们可以取到相应解析后的值(如返回值类型,sql,入参等信息都是解析好封装在相应的类中)。如果是我们指定的返回类型,也就是意味着这条sql,是用自定义的通用mapper去执行查询的,所以我们拦截下来,自己查询,并将查询的结果,映射到入参中传来的真正返回值类型中去。如果不是自定义的返回类型,则放行,继续走mybatis原来的执行流程。(所以不会影响到mybatis本来的功能。)
这里处理返回值的时候,是通过反射,赋值到真正返回值类型中的,我采用的是驼峰式命名规则,所以我们定义POJO的是,字段与数据库对应的字段必须遵循驼峰式命名规则,否则这里可能反射去POJO中就找不到该字段了。
/**
* @program: monitor
* @description:字符串驼峰式互转
* @author: fuqiang
* @date: 2020-01-09 10:11
**/
public class CaseUtil {
/**
* 驼峰格式字符串转换为下划线格式字符串
*
* @str str
* @return
*/
public static String camelToUnderline(String str,char delimiters) {
if (str == null || "".equals(str.trim())) {
return "";
}
int len = str.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if (Character.isUpperCase(c)) {
sb.append(delimiters);
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 下划线格式字符串转换为驼峰格式字符串
*
* @str str
* @return
*/
public static String underlineToCamel(String str,char delimiters) {
if (str == null || "".equals(str.trim())) {
return "";
}
int len = str.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
c = Character.toLowerCase(c);
if (c == delimiters) {
if (++i < len) {
sb.append(Character.toUpperCase(str.charAt(i)));
}
} else {
sb.append(c);
}
}
return sb.toString();
}
}
调用
/**
* @program: monitor
* @description:
* @author: fuqiang
* @date: 2019-12-26 17:34
**/
@Service
@DS("sf_base")
public class SFOrderServiceImpl implements ISFOrderService {
@Autowired
private ExecuteMapper executeMapper;
public List<Object> getOrderInfo1() throws Exception {
//从缓存中取SQL
String sql = CacheUtils.SqlDataUtil.getSqlByTableNameAndSqlTag("SF_ORDER","SF_ORDER");
Map map = new HashMap();
//这里的 sql 对应 XML 中的 ${sql}
map.put("sql", sql);
map.put("参数1","参数1");
map.put("参数2","参数2");
//接口方式调用
List<Object> list = (List<Object>) executeMapper.executeSelectSql(map);
/*SqlSession sqlSession = sqlSessionFactory.openSession();
List<Map> list = sqlSession.selectList("executeSelectSql", map);*/
return list;
}
}
这里的入参是HashMap类型(通用mapper中定义的),可以将参数信息全都放入map中,需要保证map的key与sql中#{}中的字段对应相同即可。如果不指定resultType,这里默认返回的就是Object,所以我们一般是需要在map参数中设置resultType的值。
Map map = new HashMap();
//这里的 sql 对应 XML 中的 ${sql}
map.put("sql", sql);
//这边是需要完整的resultType类路径的,因为在拦截器中是根据类路径加载这个实体类的
map.put("resultType", "POJO路径");
map.put("参数1","参数1");
map.put("参数2","参数2");
为了入参默认传入sql和resultType,可以用下面的类型替换入参Map
/**
* @program: monitor
* @description: 使用通用sql查询时传入参数类型
* @author: fuqiang
* @date: 2020-01-09 09:44
**/
public class ParamMap extends HashMap {
private Map paramMap;
public ParamMap(String sql,String resultType) {
this.paramMap = new HashMap();
this.paramMap.put("sql",sql);
this.paramMap.put("resultType",resultType);
}
public Map getParamMap() {
return paramMap;
}
public void setParamMap(String key,Object value) {
this.paramMap.put(key,value);
}
}
注意:这里的sql只能最多包含#{}的sql,如果想要{}解析掉,然后来这里执行sql。