認識SpEL表達式


認識SpEL表達式

前言

最近項目接入苞米豆的lock4j用於分布式的鎖控制,良好的控制在多台服務器下請求分流導致的數據重復問題,使用上也比較簡單,在需要分布式鎖的方法上添加一個@Lock4j注解並添加相應的參數即可,在使用中發現其中有一個屬性keys = {"#userId", "#user.sex"},並且支持自定義重寫分布式鎖鍵的生成策略。在好奇心的驅使下,查看了默認實現的分布式鎖鍵生成策略是通過SpEL的方式解析參數信息。

SpEL概述

Spring表達式語言的全拼為Spring Expression Language,縮寫為SpEL。並且SpEL屬於spring-core模塊,不直接與Spring綁定,是一個獨立模塊,不依賴於其他模塊,可以單獨使用。

核心接口

  1. 解析器ExpressionParser,用於將字符串表達式轉換為Expression表達式對象。
  2. 表達式Expression,最后通過它的getValute方法對表達式進行計算取值。
  3. 上下文EvaluationContext,通過上下文對象結合表達式來計算最后的結果。

簡單使用

public static void main(String[] args) {
  // 創建解析器
  ExpressionParser parser = new SpelExpressionParser();
  // 解析表達式為Expression對象
  Expression expression = parser.parseExpression("'Hello' + 'World'");
  // 計算求值
  System.out.println(expression.getValue(context));
}

進行一些簡單的運算

public static void main(String[] args) {
  // 創建解析器
  ExpressionParser parser = new SpelExpressionParser();
  // 解析表達式為Expression對象
  // 進行字符串的拼接
  System.out.println(parser.parseExpression("'Hello' + 'World'").getValue(String.class));
  // 簡單的運算
  System.out.println(parser.parseExpression("1+2").getValue());
  // 簡單的比較
  System.out.println(parser.parseExpression("1>2").getValue());
  // 稍微復雜一點的比較
  System.out.println(parser.parseExpression("2>1 and (!true)").getValue());
}

通過ParseContext對象設置自定義的解析規則:這里設置表達式的解析前綴為#{解析后綴為},最后通過表達式對象expression.getValue()獲取到表達式中的值。

public static void main(String[] args) {
  ExpressionParser parser = new SpelExpressionParser();
  ParserContext parserContext = new ParserContext() {
    @Override
    public boolean isTemplate() {
      return true;
    }

    @Override
    public String getExpressionPrefix() {
      return "#{";
    }

    @Override
    public String getExpressionSuffix() {
      return "}";
    }
  };
  String template = "#{'Hello'}#{'World!'}";
  Expression expression = parser.parseExpression(template, parserContext);
  System.out.println(expression.getValue());
}

還有很多不同的取值方式,比如參數(上下文)是個對象,獲取這個對象中的某個屬性;或者參數是一個List獲取某一個索引值;又或者是一個Map對象,根據某個Key獲取對應的值等等。

實際應用

​ 如果平時有使用Spring框架應該都會有用到比如@Value注解,就是通過SpEL方式進行賦值。

public class UserFacade {
		
  	// 獲取字符串tom 
    @Value("#{'tom'}")
    private String name;
  	
  	// 獲取bean對象的屬性
    @Value("#{user.value}")
    private String value;
}

在比如接觸過Spring Security或者Shiro等身份驗證和授權的框架中,對不同的角色有不同的接口權限,會使用到如下場景,其中對@PreAuthorize("hasAuthority('ROLE_DMIN'))hasAuthority('ROLE_ADMIN')就是通過SpEL進行參數解析后,對當前用戶的角色進行校驗。

@RestController
@RequestMapping("/admin/user")
public class UserController {

    /**
     * 擁有管理員權限可查看任何用戶信息,否則只能查看自己的信息
     */
    @PreAuthorize("hasAuthority('ROLE_ADMIN'))
    @PostMapping("/getUserById/{userId}")
    public Result<List<SysUser>> getUserById(String userId) {
        return new Result<>(userFacade.getUserById(userId));
    }
}

重構

​ 之前在項目中記錄系統中一些敏感接口的請求日志信息,采用的是AOP的方式,在請求進入控制層之前攔截進入AOP的切面方法,但是記錄的日志部分關鍵信息需要從請求的參數中獲取,在之前的實現中是通過約定一種表達式,對應列表ListMapbean對象的取值是自實現,且僅僅支持二級取值,確實在使用上有很大的缺陷。這種場景下,就可以使用SpEL進行方法參數解析,省了重復造輪子的過程,且使用上更為靈活。

SpEL結合AOP重構請求日志保存,這邊只做簡單的通過SpEL方式進行對象等取值處理,不考慮具體實際場景中的復雜業務邏輯。

/**
 * 測試控制層
 * @Author: xiaocainiaoya
 * @Date: 2021/04/20 23:06:06
 **/
@RestController
@RequestMapping("basic")
@Api(tags = "測試")
public class BasicVersionController {

    @ApiOperation(value="測試",notes="測試")
    @PostMapping("test")
    @ControllerMethodLog(name = "測試保存請求日志", description = "測試保存請求日志")
    @LogAssistParams(value={
            @LogAssistParam(logField="projectName",objField="#projectInfo.id"),
            @LogAssistParam(logField="id",objField="#projectInfo.projectName")
    })
    public RestResponse<ProjectInfo> test(@RequestBody ProjectInfo projectInfo){
        return null;
    }
}

AOP切面類

/**
 * 攔截日志
 *
 * @Author: xiaocainiaoya
 * @Date: 2021/04/20 23:08:28
 **/
@Aspect
@Component
@Slf4j
public class OperationTestLogAspect {

   @Autowired
   private OperationLogFacade operationLogFacade;

   /**
   * 此處的切點是注解的方式
   */
   @Pointcut("@annotation(cn.com.xiaocainiaoya.annotation.ControllerMethodLog)")
   public void operationLog() {
   }

   @Around("operationLog()")
   public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      OperationLog operationLog = new OperationLog();
      operationLog.setStatus(1);// 默認調用成功,異常時修改為調用失敗

      Object thing = null;
      try {
         // 執行切入方法內容
         thing = joinPoint.proceed();
         operationLog.setOperEndTime(DateTime.now().toJdkDate());
         return thing;
      } catch (Throwable e) {
         log.error(e.getMessage(), e);
         operationLog.setStatus(0);//發生異常時定義為調用失敗
         operationLog.setResultContext(e.getMessage());
         throw e;
      } finally {
         insertOperationLog(operationLog, joinPoint, thing);
      }
   }

   private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

   private static final ExpressionParser PARSER = new SpelExpressionParser();

   /**
   *   插入操作日志
   *
   * @Author: xiaocainiaoya
   * @Date: 2021/04/20 23:11:28
   * @param operationLog 日志基礎信息
   * @param joinPoint 攔截切入點信息
   * @param thing 攔截函數返回值
   * @return:
   **/
   private void insertOperationLog(OperationLog operationLog, ProceedingJoinPoint joinPoint, Object thing) {
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      ControllerMethodLog methodAnnotation = signature.getMethod().getAnnotation(ControllerMethodLog.class);
      Api typeAnnotation = (Api) signature.getDeclaringType().getAnnotation(Api.class);
      //注釋不完整不進行日志記錄操作
      if (methodAnnotation == null || typeAnnotation == null) {
         return;
      }
      LogAssistParams logAssistParams = signature.getMethod().getAnnotation(LogAssistParams.class);
      if(methodAnnotation == null){
         return ;
      }
      LogAssistParam[] assistParams = logAssistParams.value();
      if(ObjectUtil.isNull(assistParams) || assistParams.length == 0){
         return ;
      }
      for(int i = 0; i < assistParams.length; i++){
         /**
          * 重點在這,通過MethodBasedEvaluationContext構建解析器ExpressionParser的上下文, 底層邏輯也是通過ParameterNameDiscoverer反射獲取對應的屬性值
          */
         EvaluationContext context = new MethodBasedEvaluationContext((Object) null, signature.getMethod(), joinPoint.getArgs(), NAME_DISCOVERER);
         String value = (String)PARSER.parseExpression(assistParams[i].objField()).getValue(context);
         ReflectUtil.setFieldValue(operationLog, assistParams[i].logField(), value);
      }
      operationLogFacade.insertSelective(operationLog);
   }
}

博客地址:https://xiaocainiaoya.github.io/

聯系方式:xiaocainiaoya@foxmail.com

掃碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM