場景
系統需要記錄日誌然後保存到數據庫,保存的字段需要從請求信息中提取字段字段,不同的請求需要提取的字段不一樣
實現
請求a 的請求信息 {“aaa”:“aaa”,“bbbb”:“bbbb”},需要提取的字段爲 aaa,代碼實體中屬性名也爲aaa,
存入擴展字段一中,擴展字段的名爲 PARAM1,則代碼中需要將aaa與PARAM1對應起來
正常邏輯可以這麼做:1.插入數據的時候可以 insert into log(PARAM1) values (#{aaa})
2.查詢的時候 select * from log where PARAM1 like “%XXX%” ,然後編寫 resultMap 進行字段映射
這樣的弊端:當有多個字段時,對應起來會讓人奔潰,多個擴展字段與抽取字段對應操作繁瑣,很容易出錯
解決辦法:
1.在屬性 aaa 上加入自定義註解 ,註解中標明擴展字段名爲 PARAM1
2.mybatis攔截器中對sql語句中aaa內容替換成註解上註明的PARAM1
效果:
1.插入數據的時候可以 insert into log(aaa) values (#{aaa})
2.查詢的時候 select * from log where aaa like “%XXX%” , 然後編寫 resultMap 進行字段映射
需要注意的是:查詢時sql語句不需要寫擴展字段名,但是 要寫 resultMap 進行字段映射
註解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtendColumnName {
String value() ;
}
數據庫定義
CREATE TABLE "RJGF_KMS"."KMS_REQUEST_LOG"
( "ID" NUMBER,
"REQUEST_URL" VARCHAR2(100),
"REQUEST_DATA" VARCHAR2(1000),
"IP" VARCHAR2(20),
"RESPONSE_DATA" VARCHAR2(1000),
"TYPE_CODE" VARCHAR2(50),
"RECORD_TIME" TIMESTAMP (6),
"PARAM1" VARCHAR2(255),
"PARAM2" VARCHAR2(255),
"PARAM3" VARCHAR2(255),
"PARAM4" VARCHAR2(255),
"PARAM5" VARCHAR2(255)
) SEGMENT CREATION IMMEDIATE
PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
STORAGE(INITIAL 65536 NEXT 8192 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
實體
public class PsamRequestLogEntity extends KmsRequestLog {
@ExtendColumnName("PARAM1")
private String psamNo;
@ExtendColumnName("PARAM2")
private String code;
@ExtendColumnName("PARAM3")
private String lanNo;
}
攔截器
@Component
@Intercepts(
{
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
}
)
public class RequestLogExtendColumnInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 獲取sql
String sql = getSqlByInvocation(invocation);
if (StringUtils.isBlank(sql)) {
return invocation.proceed();
}
// 找出需要替換的字段
Map<String, ExtendColumnName> extendColumnNames = getExtendColumn(invocation);
if (extendColumnNames.size() == 0){
return invocation.proceed();
}
// sql交由處理類處理 對sql語句進行處理
String newSql = getExtendSql(extendColumnNames,sql);
// 包裝sql後,重置到invocation中
resetSql2Invocation(invocation, newSql);
return invocation.proceed();
}
private String getExtendSql(Map<String, ExtendColumnName> extendColumnNames, String sql) {
String retVal = sql;
for (Map.Entry<String, ExtendColumnName> stringExtendColumnNameEntry : extendColumnNames.entrySet()) {
String key = stringExtendColumnNameEntry.getKey();
ExtendColumnName value = stringExtendColumnNameEntry.getValue();
// 將sql中的屬性名 替換成 註解上的名字
retVal = retVal.replace(key,value.value());
}
return retVal;
}
/**
* 獲取需要替換字段名的屬性
* @param invocation
* @return
*/
private Map<String, ExtendColumnName> getExtendColumn(Invocation invocation) {
Map<String, ExtendColumnName> retVal = new HashMap<>();
Object arg = invocation.getArgs()[1];
// 當接口的方法有多個參數時,arg[1] 的對象爲 MapperMethod.ParamMap,只有一個參數時,arg[1] 就是參數本身
if (arg instanceof MapperMethod.ParamMap){
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) arg;
Collection values = paramMap.values();
for (Object value : values) {
if (value != null) {
getExtendColumn(retVal, value);
}
}
} else {
getExtendColumn(retVal,arg);
}
return retVal;
}
private void getExtendColumn(Map<String, ExtendColumnName> retVal, Object arg) {
Field[] declaredFields = getAllFields(arg);
for (Field declaredField : declaredFields) {
ExtendColumnName annotation = declaredField.getAnnotation(ExtendColumnName.class);
if (null != annotation){
// 如果屬性上有 ExtendColumnName 該註解,就放入返回值中
retVal.put(declaredField.getName(),annotation);
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 獲取sql語句
* @param invocation
* @return
*/
private String getSqlByInvocation(Invocation invocation) {
final Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
BoundSql boundSql = ms.getBoundSql(parameterObject);
return boundSql.getSql();
}
/**
* 包裝sql後,重置到invocation中
* @param invocation
* @param sql
* @throws SQLException
*/
private void resetSql2Invocation(Invocation invocation, String sql) throws SQLException {
final Object[] args = invocation.getArgs();
MappedStatement statement = (MappedStatement) args[0];
Object parameterObject = args[1];
BoundSql boundSql = statement.getBoundSql(parameterObject);
MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSql));
MetaObject msObject = MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
msObject.setValue("sqlSource.boundSql.sql", sql);
args[0] = newStatement;
// 如果參數個數爲6,還需要處理 BoundSql 對象
if (6 == args.length){
BoundSql boundSqlArg = (BoundSql) args[5];
// 該對象沒有提供對sql屬性的set方法,只能通過反射進行修改
Class<? extends BoundSql> aClass = boundSql.getClass();
try {
Field field = aClass.getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSqlArg,sql);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder =
new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
public static Field[] getAllFields(Object object){
Class clazz = object.getClass();
List<Field> fieldList = new ArrayList<>();
while (clazz != null){
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
}
// 定義一個內部輔助類,作用是包裝sq
class BoundSqlSqlSource implements SqlSource {
private BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
xml
<resultMap id="PsamRequestLog" type="net.rjgf.bg.modules.sys.entity.bean.PsamRequestLogEntity">
<id column="id" property="id"></id>
<result column="record_time" property="recordTime"></result>
<result column="ip" property="ip"></result>
<result column="request_data" property="requestData"></result>
<result column="response_data" property="responseData"></result>
<result column="request_url" property="requestUrl"></result>
<result column="type_code" property="typeCode"></result>
<result column="PARAM1" property="psamNo"></result>
<result column="PARAM2" property="code"></result>
<result column="PARAM3" property="lanNo"></result>
</resultMap>
<!--查詢-->
<select id="selectPSAMLogByConditions" parameterType="net.rjgf.bg.modules.sys.entity.so.PsamRequestLogSo" resultMap="PsamRequestLog">
select * from KMS_REQUEST_LOG
<where>
<if test="so.ip != null and so.ip != ''">
and ip like concat('%',concat(#{so.ip},'%'))
</if>
<if test="so.typeCode != null and so.typeCode != ''">
and type_code like concat('%',concat(#{so.typeCode},'%'))
</if>
<if test="so.psamNo != null and so.psamNo != ''">
and psamNo like concat('%',concat(#{so.psamNo},'%'))
</if>
<if test="so.code != null and so.code != ''">
and code like concat('%',concat(#{so.code},'%'))
</if>
<if test="so.lanNo != null and so.lanNo != ''">
and lanNo like concat('%',concat(#{so.lanNo},'%'))
</if>
<if test="so.startTime != null and so.startTime != ''">
and record_time >= to_date(#{so.startTime},'yyyy-mm-dd')
</if>
<if test="so.endTime != null and so.endTime != ''">
and to_date(#{so.endTime},'yyyy-mm-dd') >= record_time
</if>
<if test="so.requestUrl != null and so.requestUrl != ''">
and request_url like concat('%',concat(#{so.requestUrl},'%'))
</if>
</where>
</select>
<!--新增Psam請求日誌-->
<insert id="insertPsamLog" parameterType="net.rjgf.kms.forward.entity.bean.PsamRequestLogEntity">
INSERT INTO KMS_REQUEST_LOG(ID,REQUEST_URL, REQUEST_DATA, IP, RESPONSE_DATA, TYPE_CODE, RECORD_TIME, psamNo,lanNo,code)
VALUES ( KMS_REQUEST_LOG_SEQ.NEXTVAL,#{requestUrl,jdbcType=VARCHAR}, #{requestData, jdbcType=VARCHAR}, #{ip, jdbcType=VARCHAR}, #{responseData, jdbcType=VARCHAR}, #{typeCode, jdbcType=VARCHAR}, #{recordTime, jdbcType=VARCHAR}, #{psamNo, jdbcType=VARCHAR}, #{lanNo, jdbcType=VARCHAR}, #{code, jdbcType=VARCHAR})
</insert>
效果
這樣就不需要管數據庫「擴展字段名」與代碼實體類中「屬性名」的對應關係了
攔截器中需要注意的地方
sql修改後要更新兩個對象 「MappedStatement」與「BoundSql」都修改下最好,因爲源碼中有時候用到了BoundSql對象來獲取sql語句