欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用mybatis攔截器處理敏感字段

 更新時間:2021年09月23日 10:35:05   作者:alleged  
這篇文章主要介紹了mybatis攔截器處理敏感字段方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

mybatis攔截器處理敏感字段

前言

由于公司業(yè)務(wù)要求,需要在不影響已有業(yè)務(wù)上對 數(shù)據(jù)庫中已有數(shù)據(jù)的敏感字段加密解密,個人解決方案利用mybatis的攔截器加密解密敏感字段

思路解析

  • 利用注解標(biāo)明需要加密解密的entity類對象以及其中的數(shù)據(jù)
  • mybatis攔截Executor.class對象中的query,update方法
  • 在方法執(zhí)行前對parameter進行加密解密,在攔截器執(zhí)行后,解密返回的結(jié)果

代碼

1、配置攔截器(interceptor后為自己攔截器的包路徑)

<plugins>
  <plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
   <property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.OracleDialect" />
  </plugin>
  <plugin interceptor="com.XXX.XXXX.service.encryptinfo.DaoInterceptor" />
 </plugins>

2、攔截器的實現(xiàn)

特別注意:因為Dao方法參數(shù)有可能單一參數(shù),多參數(shù)map形式,以及entity對象參數(shù)類型,所以不通類型需有不通的處理方式(本文參數(shù) 單一字符串和entity對象,返回的結(jié)果集 List<?> 和entity)

后續(xù)在攔截器中添加了相應(yīng)的開關(guān),控制參數(shù)是否加密查詢,解密已實現(xiàn)兼容

package com.ips.fpms.service.encryptinfo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.xxx.xxx.dao.WhiteListDao;
import com.xxx.xxx.entity.db.WhiteListEntity;
import com.xxx.xxx.service.util.SpringBeanUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
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 org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xxx.xxx.annotation.EncryptField;
import com.xxx.xxx.annotation.EncryptMethod;
import com.xxx.xxx.common.utils.CloneUtil;
import com.xxx.core.psfp.common.support.JsonUtils;
import com.xxx.xxx.service.util.CryptPojoUtils;
@Intercepts({
    @Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),
    @Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})
})
public class EncryptDaoInterceptor implements Interceptor{
	private final Logger logger = LoggerFactory.getLogger(EncryptDaoInterceptor.class);
	private WhiteListDao whiteListDao;
	static int MAPPED_STATEMENT_INDEX = 0;
	static int PARAMETER_INDEX = 1;
	static int ROWBOUNDS_INDEX = 2;
	static int RESULT_HANDLER_INDEX = 3;
	static String ENCRYPTFIELD = "1";
	static String DECRYPTFIELD = "2";
	private static final String ENCRYPT_KEY = "encry146local";
	private static final String ENCRYPT_NUM = "146";
	private static boolean ENCRYPT_SWTICH = true;
	/**
	 * 是否進行加密查詢
	 * @return 1 true 代表加密 0 false 不加密
	 */
	private boolean getFuncSwitch(){
		if(whiteListDao == null){
			whiteListDao = SpringBeanUtils.getBean("whiteListDao",WhiteListDao.class);
		}
		try{
			WhiteListEntity entity = whiteListDao.selectOne(ENCRYPT_KEY,ENCRYPT_NUM);
			if(entity!=null && "1".equals(entity.getFlag())){
				ENCRYPT_SWTICH = true;
			}else{
				ENCRYPT_SWTICH = false;
			}
		}catch (Exception e){
			logger.error(this.getClass().getName()+".getFuncSwitch 白名單查詢異常,默認本地數(shù)據(jù)加密關(guān)閉[]:",e.getStackTrace());
			return false;
		}
		return ENCRYPT_SWTICH;
	}
	/**
	 * 校驗執(zhí)行器方法 是否在白名單中
	 * @param statementid
	 * @return true 包含 false 不包含
	 */
	private boolean isWhiteList(String statementid){
		boolean result = false;
		String whiteStatementid = "com.ips.fpms.dao.WhiteListDao.selectOne";
		if(whiteStatementid.indexOf(statementid)!=-1){
			result = true;
		}
		return result;
	}
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		logger.info("EncryptDaoInterceptor.intercept開始執(zhí)行==> ");
		MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
		Object parameter = invocation.getArgs()[PARAMETER_INDEX];
		logger.info(statement.getId()+"未加密參數(shù)串:"+JsonUtils.object2jsonString(CloneUtil.deepClone(parameter)));
		/*
		 *
		 * 判斷是否攔截白名單 或 加密開關(guān)是否配置,
		 * 如果不在白名單中,并且本地加密開關(guān) 已打開 執(zhí)行參數(shù)加密
		 *
		 *  */
		if(!isWhiteList(statement.getId()) && getFuncSwitch()){
			parameter = encryptParam(parameter, invocation);
			logger.info(statement.getId()+"加密后參數(shù):"+JsonUtils.object2jsonString(CloneUtil.deepClone(parameter)));
		}
		invocation.getArgs()[PARAMETER_INDEX] = parameter;
		Object returnValue = invocation.proceed();
		logger.info(statement.getId()+"未解密結(jié)果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue)));
		returnValue = decryptReslut(returnValue, invocation);
		logger.info(statement.getId()+"解密后結(jié)果集:"+JsonUtils.object2jsonString(CloneUtil.deepClone(returnValue)));
		logger.info("EncryptDaoInterceptor.intercept執(zhí)行結(jié)束==> ");
		return returnValue;
	}
	/**
	 * 解密結(jié)果集
	 * @param @param returnValue
	 * @param @param invocation
	 * @param @return
	 * @return Object
	 * @throws 
	 *
	 */
	public Object decryptReslut(Object returnValue,Invocation invocation){
		MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
		if(returnValue!=null){
	       	 if(returnValue instanceof ArrayList<?>){
	       		 List<?> list = (ArrayList<?>) returnValue;
	       		 List<Object> newList  = new ArrayList<Object>();
	       		 if (1 <= list.size()){
	       			 for(Object object:list){
	       				 Object obj = CryptPojoUtils.decrypt(object);
	       				 newList.add(obj);
	       			 }
	       			 returnValue = newList;
	       		 }
	       	 }else if(returnValue instanceof Map){
	       		String[] fields = getEncryFieldList(statement,DECRYPTFIELD);
	       		if(fields!=null){
	       			returnValue = CryptPojoUtils.getDecryptMapValue(returnValue,fields);
	       		}
	       	 }else{
	       		 returnValue = CryptPojoUtils.decrypt(returnValue);
	       	 }
        }
		return returnValue;
	}
	/***
	 * 針對不同的參數(shù)類型進行加密
	 * @param @param parameter
	 * @param @param invocation
	 * @param @return
	 * @return Object
	 * @throws 
	 *
	 */
	public Object encryptParam(Object parameter,Invocation invocation){
		MappedStatement statement = (MappedStatement) invocation.getArgs()[MAPPED_STATEMENT_INDEX];
		try {
			if(parameter instanceof String){
				if(isEncryptStr(statement)){
					parameter = CryptPojoUtils.encryptStr(parameter);
				}
			}else if(parameter instanceof Map){
				String[] fields = getEncryFieldList(statement,ENCRYPTFIELD);
				if(fields!=null){
					parameter = CryptPojoUtils.getEncryptMapValue(parameter,fields);
				}
			}else{
				parameter = CryptPojoUtils.encrypt(parameter);
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			logger.info("EncryptDaoInterceptor.encryptParam方法異常==> " + e.getMessage());
		}
		return parameter;
	}
	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}
	@Override
	public void setProperties(Properties properties) {
	}
	/**
	 * 獲取參數(shù)map中需要加密字段
	 * @param statement
	 * @param type
	 * @return List<String>
	 * @throws 
	 *
	 */
	private String[] getEncryFieldList(MappedStatement statement,String type){
		String[] strArry = null;
		Method method = getDaoTargetMethod(statement);
		Annotation annotation =method.getAnnotation(EncryptMethod.class);
		if(annotation!=null){
			if(type.equals(ENCRYPTFIELD)){
				String encryString = ((EncryptMethod) annotation).encrypt();
				if(!"".equals(encryString)){
					strArry =encryString.split(",");
				}
			}else if(type.equals(DECRYPTFIELD)){
				String encryString = ((EncryptMethod) annotation).decrypt();
				if(!"".equals(encryString)){
					strArry =encryString.split(",");
				}
			}else{
				strArry = null;
			}
		}
		return strArry;
	}
	/**
	 * 獲取Dao層接口方法
	 * @param @return
	 * @return Method
	 * @throws 
	 *
	 */
	private Method getDaoTargetMethod(MappedStatement mappedStatement){
		Method method = null;
		try {
			String namespace = mappedStatement.getId();
		    String className = namespace.substring(0,namespace.lastIndexOf("."));
		    String methedName= namespace.substring(namespace.lastIndexOf(".") + 1,namespace.length());
			Method[] ms = Class.forName(className).getMethods();
			for(Method m : ms){
		        if(m.getName().equals(methedName)){
		        	method = m;
		        	break;
		        }
			}
		} catch (SecurityException e) {
			e.printStackTrace();
			logger.info("EncryptDaoInterceptor.getDaoTargetMethod方法異常==> " + e.getMessage());
			return method;
			
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			logger.info("EncryptDaoInterceptor.getDaoTargetMethod方法異常==> " + e.getMessage());
			return method;
		}
		return method;
	}
	/**
	 * 判斷字符串是否需要加密
	 * @param @param mappedStatement
	 * @param @return
	 * @return boolean
	 * @throws 
	 *
	 */
	private boolean isEncryptStr(MappedStatement mappedStatement) throws ClassNotFoundException{
		boolean reslut = false;
		try {
				Method m = getDaoTargetMethod(mappedStatement);
	        	m.setAccessible(true);
	        	Annotation[][] parameterAnnotations = m.getParameterAnnotations();
		        	 if (parameterAnnotations != null && parameterAnnotations.length > 0) { 
		        		 for (Annotation[] parameterAnnotation : parameterAnnotations) {  
		        	         for (Annotation annotation : parameterAnnotation) {  
		        	          if (annotation instanceof EncryptField) {  
		        	        	  reslut = true;
		        	          }
		        	      }
		        	 }
	        	 }
		} catch (SecurityException e) {
			e.printStackTrace();
			logger.info("EncryptDaoInterceptor.isEncryptStr異常:==> " + e.getMessage());
			reslut = false;
		}
		return reslut;
	}
}

2、注解的entity對象

//是否需要加密解密對象
@EncryptDecryptClass
public class MerDealInfoRequest extends PagingReqMsg {
//屬性定義
 @EncryptField
    @DecryptField
    private String cardNo;
}

3、dao方法中的單一參數(shù)

List<Dealer> selectDealerAndMercode(@EncryptField String idcardno);

4、封裝的工具類(EncryptDecryptUtil.decryptStrValue 解密方法 EncryptDecryptUtil.decryptStrValue 加密方法)

package com.xxx.xxx.service.util;
import java.lang.reflect.Field;
import java.util.ArrayList;
import org.apache.commons.lang.StringUtils;
import org.apache.pdfbox.Encrypt;
import org.apache.poi.ss.formula.functions.T;
import com.xxx.xxx.annotation.DecryptField;
import com.xxx.xxx.annotation.EncryptDecryptClass;
import com.xxx.xxx.annotation.EncryptField;
import com.xxx.xxx.common.utils.EncryptDecryptUtil;
public class CryptPojoUtils {
	 /**
     * 對象t注解字段加密
     * @param t
     * @param <T>
     * @return
     */
    public static <T> T encrypt(T t) {
    	if(isEncryptAndDecrypt(t)){
	        Field[] declaredFields = t.getClass().getDeclaredFields();
	        try {
	            if (declaredFields != null && declaredFields.length > 0) {
	                for (Field field : declaredFields) {
	                    if (field.isAnnotationPresent(EncryptField.class) && field.getType().toString().endsWith("String")) {
	                        field.setAccessible(true);
	                        String fieldValue = (String) field.get(t);
	                        if (StringUtils.isNotEmpty(fieldValue)) {
	    	                    field.set(t, EncryptDecryptUtil.encryStrValue(fieldValue) );
	                        }
	                        field.setAccessible(false);
	                    }
	                }
	            }
	        } catch (IllegalAccessException e) {
	            throw new RuntimeException(e);
	        }
    	}
        return t;
    }
    /**
     * 加密單獨的字符串
     *
     * @param @param t
     * @param @return
     * @return T
     * @throws 
     *
     */
    public static <T> T EncryptStr(T t){
    	if(t instanceof String){
    		t = (T) EncryptDecryptUtil.encryStrValue((String) t);
    	}
    	return t;
    }
    /**
     * 對含注解字段解密
     * @param t
     * @param <T>
     */
    public static <T> T decrypt(T t) {
    	if(isEncryptAndDecrypt(t)){
	        Field[] declaredFields = t.getClass().getDeclaredFields();
	        try {
	            if (declaredFields != null && declaredFields.length > 0) {
	                for (Field field : declaredFields) {
	                    if (field.isAnnotationPresent(DecryptField.class) && field.getType().toString().endsWith("String")) {
	                        field.setAccessible(true);
	                        String fieldValue = (String)field.get(t);
	                        if(StringUtils.isNotEmpty(fieldValue)) {
	                            field.set(t, EncryptDecryptUtil.decryptStrValue(fieldValue));
	                        }
	                    }
	                }
	            }
	        } catch (IllegalAccessException e) {
	            throw new RuntimeException(e);
	        }
        }
         return t;
    }
    /**
     * 判斷是否需要加密解密的類
     * @param @param t
     * @param @return
     * @return Boolean
     * @throws 
     *
     */
    public static <T> Boolean isEncryptAndDecrypt(T t){
    	Boolean reslut = false;
    	if(t!=null){
    		Object object = t.getClass().getAnnotation(EncryptDecryptClass.class);
        	if(object != null){
        		reslut = true;
        	}
    	}
		return reslut;
    }
}

趟過的坑(敲黑板重點)

1、在實現(xiàn)上述功能后的測試中,其中select查詢方法的參數(shù)在加密成功后,但是Executor執(zhí)行器執(zhí)行方法參數(shù)依舊為未加密的參數(shù),找各路大神都沒有解決的思路,最后發(fā)現(xiàn)項目中引用了開源的分頁插件, OffsetLimitInterceptor攔截器把參數(shù)設(shè)置成為final的,所以自定義攔截器沒有修改成功這個sql參數(shù);

解決辦法:自定義攔截器放到這個攔截器后,自定義攔截器先執(zhí)行就可以了

<plugins>
     //就是這個攔截器 
  <plugin interceptor="com.github.miemiedev.mybatis.paginator.OffsetLimitInterceptor">
   <property name="dialectClass" value="com.github.miemiedev.mybatis.paginator.dialect.OracleDialect" />
  </plugin>
  <plugin interceptor="com.ips.fpms.service.encryptinfo.DaoInterceptor" />
 </plugins>
public Object intercept(final Invocation invocation) throws Throwable {
        final Executor executor = (Executor) invocation.getTarget();
        final Object[] queryArgs = invocation.getArgs();
        final MappedStatement ms = (MappedStatement)queryArgs[MAPPED_STATEMENT_INDEX];
        //攔截器把參數(shù)設(shè)置成為final的,所以自定義攔截器沒有修改到這個參數(shù)
        final Object parameter = queryArgs[PARAMETER_INDEX];
        final RowBounds rowBounds = (RowBounds)queryArgs[ROWBOUNDS_INDEX];
        final PageBounds pageBounds = new PageBounds(rowBounds);
        final int offset = pageBounds.getOffset();
        final int limit = pageBounds.getLimit();
        final int page = pageBounds.getPage();
        .....省略代碼....
        }

2、數(shù)據(jù)庫存量數(shù)據(jù)處理

在添加攔截器后,必須對數(shù)據(jù)庫的存量數(shù)據(jù)進行處理,如果不進行處理,查詢參數(shù)已經(jīng)加密,但是數(shù)據(jù)依舊是明文,會導(dǎo)致查詢條件不匹配

mybatis Excutor 攔截器的使用

這里要講的巧妙用法是用來實現(xiàn)在攔截器中執(zhí)行額外 MyBatis 現(xiàn)有方法的用法。

并且會提供一個解決攔截Executor時想要修改MappedStatement時解決并發(fā)的問題。

這里假設(shè)一個場景

實現(xiàn)一個攔截器,記錄 MyBatis 所有的 insert,update,delete 操作,將記錄的信息存入數(shù)據(jù)庫。

這個用法在這里就是將記錄的信息存入數(shù)據(jù)庫。

實現(xiàn)過程的關(guān)鍵步驟和代碼

1.首先在某個 Mapper.xml 中定義好了一個往日志表中插入記錄的方法,假設(shè)方法為id="insertSqlLog"。

2.日志表相關(guān)的實體類為SqlLog.

3.攔截器簽名:

@Intercepts({@org.apache.ibatis.plugin.Signature(
    type=Executor.class, 
    method="update", 
    args={MappedStatement.class, Object.class})})
public class SqlInterceptor implements Interceptor

4.接口方法簡單實現(xiàn):

public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    MappedStatement ms = (MappedStatement) args[0];
    Object parameter = args[1];
    SqlLog log = new SqlLog();
    Configuration configuration = ms.getConfiguration();
    Object target = invocation.getTarget();
    StatementHandler handler = configuration.newStatementHandler((Executor) target, ms, 
              parameter, RowBounds.DEFAULT, null, null);
    BoundSql boundSql = handler.getBoundSql();
    //記錄SQL
    log.setSqlclause(boundSql.getSql());
    //執(zhí)行真正的方法
    Object result = invocation.proceed();
    //記錄影響行數(shù)
    log.setResult(Integer.valueOf(Integer.parseInt(result.toString())));
    //記錄時間
    log.setWhencreated(new Date());
    //TODO 還可以記錄參數(shù),或者單表id操作時,記錄數(shù)據(jù)操作前的狀態(tài)
    //獲取insertSqlLog方法
    ms = ms.getConfiguration().getMappedStatement("insertSqlLog");
    //替換當(dāng)前的參數(shù)為新的ms
    args[0] = ms;
    //insertSqlLog 方法的參數(shù)為 log
    args[1] = log;
    //執(zhí)行insertSqlLog方法
    invocation.proceed();
    //返回真正方法執(zhí)行的結(jié)果
    return result;
}

重點

MappedStatement是一個共享的緩存對象,這個對象是存在并發(fā)問題的,所以幾乎任何情況下都不能去修改這個對象(通用Mapper除外),想要對MappedStatement做修改該怎么辦呢?

并不難,Executor中的攔截器方法參數(shù)中都有MappedStatement ms,這個ms就是后續(xù)方法執(zhí)行要真正用到的MappedStatement,這樣一來,問題就容易解決了,根據(jù)自己的需要,深層復(fù)制MappedStatement對象中自己需要修改的屬性,然后修改這部分屬性,之后將修改后的ms通過上面代碼中args[0]=ms這種方式替換原有的參數(shù),這樣就能實現(xiàn)對ms的修改而且不會有并發(fā)問題了。

這里日志的例子就是一個更簡單的應(yīng)用,并沒有創(chuàng)建ms,只是獲取了一個新的ms替換現(xiàn)有的ms,然后去執(zhí)行。

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java并發(fā)編程學(xué)習(xí)之ThreadLocal源碼詳析

    Java并發(fā)編程學(xué)習(xí)之ThreadLocal源碼詳析

    這篇文章主要給大家介紹了關(guān)于Java并發(fā)編程學(xué)習(xí)之源碼分析ThreadLocal的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-06-06
  • 項目管理利器-Maven(Windows安裝)圖文教程

    項目管理利器-Maven(Windows安裝)圖文教程

    下面小編就為大家?guī)硪黄椖抗芾砝?Maven(Windows安裝)圖文教程。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-06-06
  • servlet3文件上傳操作

    servlet3文件上傳操作

    這篇文章主要介紹了servlet3文件上傳操作的相關(guān)資料,需要的朋友可以參考下
    2017-11-11
  • Spring常用注解 使用注解來構(gòu)造IoC容器的方法

    Spring常用注解 使用注解來構(gòu)造IoC容器的方法

    下面小編就為大家分享一篇Spring常用注解 使用注解來構(gòu)造IoC容器的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-01-01
  • Java之Spring Boot創(chuàng)建和使用

    Java之Spring Boot創(chuàng)建和使用

    Spring 的誕生就是為了簡化 Java 程序的開發(fā)的.Spring Boot 的誕生就是為了簡化 Spring 程序開發(fā)的,對Springboot感興趣的同學(xué)可以借鑒本文
    2023-04-04
  • 更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator

    更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator

    這篇文章主要為大家介紹了更簡單更高效的Mybatis?Plus最新代碼生成器AutoGenerator使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • springboot整合spring-data-redis遇到的坑

    springboot整合spring-data-redis遇到的坑

    使用springboot整合redis,使用默認的序列化配置,然后使用redis-client去查詢時查詢不到相應(yīng)的key.問題出在哪,怎么解決呢?下面小編給大家?guī)砹藄pringboot整合spring-data-redis遇到的坑,需要的的朋友參考下吧
    2017-04-04
  • java 文件名截取方法

    java 文件名截取方法

    在實際開發(fā)應(yīng)用中會應(yīng)到截取文件名,本文將介紹java中文件名的截取,需要了解的朋友可以參考下
    2012-11-11
  • SpringBoot項目中使用redis緩存的方法步驟

    SpringBoot項目中使用redis緩存的方法步驟

    本篇文章主要介紹了SpringBoot項目中使用redis緩存的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-12-12
  • SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate

    SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate

    Spring?Boot?Data(數(shù)據(jù))?Redis?中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate的子類,兩個方法基本一致。本文介紹了SpringBoot整合Redis使用RedisTemplate和StringRedisTemplate的方法,需要的可以參考一下
    2022-12-12

最新評論