Spring 表达式语言(SpEL)入门实战

1、文本表达式

文本表达式支持字符串、 日期 、 数字(正数 、 实数及十六进制数) 、 布尔类型及 null。其中的字符表达式可使用单引号来表示,形如:‘Deniro’。如果表达式中包含单引号或者双引号字符,那么可以使用转义字符 /。

ExpressionParser parser = new SpelExpressionParser();

//字符串解析
String str = (String) parser.parseExpression("'你好'").getValue();
System.out.println(str);

//整型解析
int intVal = (Integer) parser.parseExpression("0x2F").getValue();
System.out.println(intVal);

//双精度浮点型解析
double doubleVal = (Double) parser.parseExpression("4329759E+22").getValue();
System.out.println(doubleVal);


//布尔型解析
boolean booleanVal = (boolean) parser.parseExpression("true").getValue();
System.out.println(booleanVal);

输出结果:

你好
47
4.329759E28
true

数字支持负数 、小数、科学记数法、八进制数和十六进制数 。 默认情况下,实数使用 Double.parseDouble() 进行表达式类型转换 。

2、对象属性表达式

在 SpEL 中,我们可以使用对象属性路径(形如类名.属性名.属性名)来访问对象属性的值。
假设有一个账号类,Account.java:

@Data
public class Account {
    private String name;
    private int footballCount;
    private Friend friend;
}

它包含姓名 name、足球数 footballCount 和一个朋友 friend 属性。friend 属性是一个 Friend 类:

@Data
public class Friend {
    private String name;
}

解析对象属性表达式:

//初始化对象
Account account=new Account("Deniro");
account.setFootballCount(10);
account.addFriend(new Friend("Jack"));

//解析器
ExpressionParser parser  = new SpelExpressionParser();
//解析上下文
EvaluationContext context=new StandardEvaluationContext(account);

//获取不同类型的属性
String name= (String) parser.parseExpression("Name").getValue(context);
System.out.println(name);
int count= (Integer) parser.parseExpression("footballCount+1").getValue(context);
System.out.println(count);

//获取嵌套类中的属性
String friend= (String) parser.parseExpression("friend.name").getValue(context);
System.out.println(friend);

输出结果:

Deniro
11
Jack

SpEL 解析器适应力强,属性名首字母大小写均可。
解析对象表达式时,需要传入 EvaluationContext 上下文参数。

3、数组、List 和 Map 表达式

数组表达式支持 Java 创建数组的语法,形如 new int[]{3,4,5},数组项之间以逗号作为分隔符。注意:目前还不支持多维数组。Map 表达式以键值对的方式来定义,形如

{name:'deniro',footballCount:10}
//解析器
ExpressionParser parser  = new SpelExpressionParser();

//解析一维数组
int[] oneArray = (int[]) parser.parseExpression("new int[]{3,4,5}").getValue();
System.out.println("一维数组开始:");
for (int i : oneArray) {
    System.out.println(i);
}
System.out.println("一维数组结束");

//这里会抛出 SpelParseException
//int[][] twoArray = (int[][]) parser.parseExpression("new int[][]{3,4,5}{3,4,5}") .getValue();

//解析 list
List list = (List) parser.parseExpression("{3,4,5}").getValue();
System.out.println("list:" + list);

//解析 Map
Map map = (Map) parser.parseExpression("{account:'deniro',footballCount:10}") .getValue();
System.out.println("map:" + map);

//解析对象中的 list
final Account account = new Account("Deniro");
Friend friend1 = new Friend("Jack");
Friend friend2 = new Friend("Rose");
List<Friend> friends = new ArrayList<>();
friends.add(friend1);
friends.add(friend2);
account.setFriends(friends);
EvaluationContext context = new StandardEvaluationContext(account);
String friendName = (String) parser.parseExpression("friends[0].name")
        .getValue(context);
System.out.println("friendName:" + friendName);
```java
从数组与 List 获取值,可以在括号内指定索引来获取,形如上例中的 friends[0]。Map 中可通过键名来获取,形如 xxx['xxx']。
输出结果:

一维数组开始:
3
4
5
一维数组结束
list:[3, 4, 5]
map:{account=deniro, footballCount=10}
friendName:Jack

### 4、方法表达式
SpEL 支持调用有访问权限的方法,这些方法包括对象方法、静态方法,而且支持可变方法参数。除此之外,还可以调用 String 类型中的所有可访问方法,比如 
```java
String.contains('xxx')//解析器
ExpressionParser parser  = new SpelExpressionParser();

//调用 String 方法
boolean isEmpty = parser.parseExpression("'Hi,everybody'.contains('Hi')").getValue. (Boolean.class);
System.out.println("isEmpty:" + isEmpty);

/**
 * 调用对象相关方法
 */
final Account account = new Account("Deniro");
EvaluationContext context = new StandardEvaluationContext(account);

//调用公开方法
parser.parseExpression("setFootballCount(11)").getValue(context, Boolean.class);
System.out.println("getFootballCount:" + account.getFootballCount());

//调用私有方法,抛出 SpelEvaluationException: EL1004E: Method call: Method write() cannot be found on net.deniro.spring4.spel.Account type
//        parser.parseExpression("write()").getValue(context,Boolean
//                .class);



//调用静态方法
parser.parseExpression("read()").getValue(context, Boolean.class);

//调用待可变参数的方法
parser.parseExpression("addFriendNames('Jack','Rose')").getValue(context, Boolean.class);

注意:调用对象的私有方法会抛出异常。
输出结果:

isEmpty:true
getFootballCount:11
读书
friendName:Jack
friendName:Rose

5、操作符表达式

5.1 关系操作符

SpEL 支持 Java 标准操作符:等于、不等于、小于、小等于、大于、大等于、正则表达式和 instanceof 操作符。

//解析器
ExpressionParser parser = new SpelExpressionParser();

//数值比较
boolean result=parser.parseExpression("2>1").getValue(Boolean.class);
System.out.println("2>1:"+result);

//字符串比较
result=parser.parseExpression("'z'>'a'").getValue(Boolean.class);
System.out.println("'z'>'a':"+result);

//instanceof 运算符
result=parser.parseExpression("'str' instanceof T(String)").getValue(Boolean.class);
System.out.println("'str' 是否为字符串 :"+result);

result=parser.parseExpression("1 instanceof T(Integer)").getValue(Boolean.class);
System.out.println("1 是否为整型 :"+result);

//正则表达式
result=parser.parseExpression("22 matches '\\d{2}'").getValue(Boolean.class);
System.out.println("22 是否为两位数字 :"+result);

输出结果:

2>1:true
'z'>'a':true
'str' 是否为字符串 :true
1 是否为整型 :true
22 是否为两位数字 :true

instanceof 操作符后面是类型表达式,格式为 T(Java 包装器类型),如整型 T(Integer)。注意:不能使用原生类型,如果这样 T(int) 会返回错误的判断结果。
matches 用于定义正则表达式,之后跟着单引号包裹着的正则表达式。

5.2 逻辑操作符

逻辑操作符支持以下操作:

逻辑操作符说明
and 或 &&与操作
or 或或操作
!非操作

注意: 在 SpEL 中,不仅支持 Java 标准的逻辑操作符,还支持 and 与 or 关键字。

//解析器
ExpressionParser parser  = new SpelExpressionParser();

//与操作
boolean result=parser.parseExpression("true && true").getValue(Boolean.class);
System.out.println("与操作:"+result);

//或操作
result=parser.parseExpression("true || false").getValue(Boolean.class);
System.out.println("或操作:"+result);

parser.parseExpression("true or false").getValue(Boolean.class);
System.out.println("或操作(or 关键字):"+result);

//非操作
result=parser.parseExpression("!false").getValue(Boolean.class);
System.out.println("非操作:"+result);

//抛出 SpelEvaluationException: EL1001E: Type conversion problem, cannot convert from java.lang.Integer to java.lang.Boolean
//parser.parseExpression("!0").getValue(Boolean.class);

输出结果:

与操作:true
或操作:true
或操作(or 关键字):true
非操作:true

注意:逻辑操作符前后运算结果必须是布尔类型,否则会抛出 SpelEvaluationException。

5.3 运算操作符

SpEL 支持 Java 运算操作符,并遵守运算符优先级规则:
| 运算操作符 | 说明 |支持的操作数类型 |
|-----|-----|
|+| 加法|数字、字符串或日期|
|-|减法|数字或日期|
|*|乘法|数字|
|/|除法|数字|
|%|取模|数字|
|^|指数幂|数字|

//加法运算
Integer iResult = parser.parseExpression("2+3").getValue(Integer.class);
System.out.println("加法运算:" + iResult);

String sResult = parser.parseExpression("'Hi,'+'everybody'").getValue(String.class);
System.out.println("字符串拼接运算:" + sResult);

//减法运算
iResult = parser.parseExpression("2-3").getValue(Integer.class);
System.out.println("减法运算:" + iResult);

//乘法运算
iResult = parser.parseExpression("2*3").getValue(Integer.class);
System.out.println("乘法运算:" + iResult);

//除法运算
iResult = parser.parseExpression("4/2").getValue(Integer.class);
System.out.println("除法运算:" + iResult);

Double dResult = parser.parseExpression("4/2.5").getValue(Double.class);
System.out.println("除法运算:" + dResult);

//求余运算
iResult = parser.parseExpression("5%2").getValue(Integer.class);
System.out.println("求余运算:" + iResult);

输出结果:

加法运算:5
字符串拼接运算:Hi,everybody
减法运算:-1
乘法运算:6
除法运算:2
除法运算:1.6
求余运算:1

6、安全导航操作符

安全导航操作符来源于 Groovy 语言,使用它能够避免空指针异常。一般在访问对象时,需要验证该对象是否为空,使用安全导航操作符就能避免繁琐的空对象验证方法。它的格式是在获取对象属性操作符“.” 之前加一个 “?”。

final Account account = new Account("Deniro");
account.addFriend(new Friend("Jack"));

//解析器
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext(account);

String friendName=parser.parseExpression("friend?.name").getValue(context,String.class);
System.out.println("friendName:"+friendName);

//设置为 null
account.setFriend(null);
friendName=parser.parseExpression("friend?.name").getValue(context,String.class);
//打印出 null
System.out.println("friendName:" + friendName);

输出结果:

friendName:Jack
friendName:null

这里会先判断 friend 对象是否为空;如果为空,则返回 “null” 字符串;否则返回需要的属性值。

7、三元操作符

SpEL 支持标准的 Java 三元操作符:<表达式 1>?<表达式 2>:<表达式 3>

ExpressionParser parser  = new SpelExpressionParser();

boolean result=parser.parseExpression("(1+2) == 3?true:false").getValue(Boolean.class);
System.out.println("result:"+result);
}

输出结果:

result:true

8、Elvis 操作符

Elvis 操作符是在 Groovy 中使用的三元操作符简化版。
在三元操作符中,我们一般需要写两次变量名,比如下面代码段中的 title:

String title="News";
String actualTitle=(title!=null)?title:"tip";

使用 Elvis 操作符后,可以将上述代码段简写为:

title?:"tip"

SpEL 支持的 Elvis 操作符格式是:?:,如果 var 变量为 null,那就取 value 值,否则就取自身的值。所以 Elvis 操作符很适合用于设置默认值。
示例:

final Account account = new Account("Deniro");

//解析器
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext(account);

String friendName=parser.parseExpression("name?:'无名'").getValue(context,String.class);
System.out.println("friendName:"+friendName);

//设置名字为 null
account.setName(null);
friendName=parser.parseExpression("name?:'无名'").getValue(context,String.class);
System.out.println("friendName:" + friendName);

输出结果:

friendName:Deniro
friendName:无名

9、赋值表达式

可以通过赋值表达式来设置属性的值,效果等同于调用 setValue() 方法。

final Account account = new Account("Deniro");

//解析器
ExpressionParser parser  = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext(account);

String name=parser.parseExpression("name='Jack'").getValue(context,String
        .class);
System.out.println("name:"+name);

10、类型操作符

类型操作符 T 可以从类路径加载指定类名称(全限定名)所对应的 Class 的实例,格式为:T(全限定类名),效果等同于 ClassLoader#loadClass()。

ExpressionParser parser   = new SpelExpressionParser();

//加载 java.lang.Integer
Class integerClass=parser.parseExpression("T(Integer)").getValue(Class
        .class);
System.out.println(integerClass==java.lang.Integer.class);

//加载 net.deniro.spring4.spel.Account
Class accountClass=parser.parseExpression("T(net.deniro.spring4.spel.Account)").getValue(Class.class);
System.out.println(accountClass==net.deniro.spring4.spel.Account.class);

//调用类静态方法
double result = (double) parser.parseExpression("T(Math).abs(-2.5)").getValue();
System.out.println("result:" + result);

输出结果:

true
true
result:2.5

我们还可以直接通过 T 操作符调用类的静态方法,格式为 T(全限定类名).静态方法名,比如上面例子中求某数的绝对值 T(Math).abs(-2.5)。
SpEL 中会使用 StandardTypeLocator#findType() 方法来加载类。 findType 方法定义如下:

public Class<?> findType(String typeName) throws EvaluationException {
        String nameToLookup = typeName;
        try {
            return ClassUtils.forName(nameToLookup, this.classLoader);
        }
        catch (ClassNotFoundException ey) {
            // try any registered prefixes before giving up
        }
        for (String prefix : this.knownPackagePrefixes) {
            try {
                nameToLookup = prefix + '.' + typeName;
                return ClassUtils.forName(nameToLookup, this.classLoader);
            }
            catch (ClassNotFoundException ex) {
                // might be a different prefix
            }
        }
        throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName);
    }

尝试直接加载类。
如果找不到,则尝试从已注册的包前缀(java.lang)下加载类。所以如果需要加载的类在 java.lang 下,那么可以直接写类名。
如果都找不到,则抛出 SpelEvaluationException 异常。

11、创建对象操作符

可以使用 new 操作符来创建一个新对象 。 除了基本类型(如整型、布尔型等)和字符串之外,创建其它类需要指明全限定类名( 包括包路径 ) 。

Account account=parser.parseExpression("new net.deniro.spring4.spel.Account" +
    "('Deniro')").getValue(Account.class);
System.out.println("name:"+account.getName());

输出结果:

name:Deniro

12、变量表达式

可以通过 #变量名 来引用在 EvaluationContext 中定义的变量。通过 EvaluationContext#setVariable(name, val) 即可定义新的变量;name 表示变量名,val 表示变量值。
如果变量是集合,比如 list,那么可以通过 #scores.[#this] 来引用集合中的元素。

Account account = new Account("Deniro");

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext(account);

//定义一个新变量,名为 newVal
context.setVariable("newVal", "Jack");

//获取变量 newVal 的值,并赋值给 User 的 name 属性
parser.parseExpression("name=#newVal").getValue(context);
System.out.println("getName:" + account.getName());

//this 操作符表示集合中的某个元素
List<Double> scores = new ArrayList<>();
scores.addAll(Arrays.asList(23.1, 82.3, 55.9));
context.setVariable("scores", scores);//在上下文中定义 scores 变量
List<Double> scoresGreat80 = (List<Double>) parser.parseExpression("#scores.?[#this>80]").getValue(context);
System.out.println("scoresGreat80:" + scoresGreat80);

输出结果:

getName:Jack
scoresGreate80:[82.3]

13、集合选择表达式

可以使用选择表达式来过滤集合,从而生成一个新的符合选择条件的集合 。它的语法是 ?[selectionExpression]。选择符合条件的结果集的第一个元素的语法为 ^ [selectionExpression] ,选择最后一个元素的语法为 $[selectionExpression]。选择表达式也可应用于 Map 。

//过滤 list 集合中的元素
final StandardEvaluationContext listContext = new
        StandardEvaluationContext(list);
List<Integer> great4List = (List<Integer>) parser.parseExpression("?[#this>4]")
        .getValue(listContext);
System.out.println("great4List:" + great4List);

//获取匹配元素中的第一个值
Integer first = (Integer) parser.parseExpression("^[#this>2]")
        .getValue(listContext);
System.out.println("first:" + first);

//获取匹配元素中的最后一个值
Integer end = (Integer) parser.parseExpression("$[#this>2]")
        .getValue(listContext);
System.out.println("end:" + end);

输出结果:

list:[3, 4, 5]
great4List:[5]
first:3
end:5

对于 List 和 Set ,是针对集合中的每一个元素进行比较的;而对于 Map,则可以指定是元素的键(key)还是元素的值进行比较的。

//过滤 Map
Map<String, Double> rank = new HashMap<>();
rank.put("Deniro", 96.5);
rank.put("Jack", 85.3);
rank.put("Lily", 91.1);
context.setVariable("Rank", rank);

//value 大于 90
Map<String,Double> rankGreat95= (Map<String, Double>) parser.parseExpression
        ("#Rank.?[value>90]").getValue(context);
System.out.println("rankGreat95:" + rankGreat95);

//key 按字母顺序,排在 L 后面
Map<String,Double> afterL= (Map<String, Double>) parser.parseExpression
        ("#Rank.?[key>'L']").getValue(context);
System.out.println("afterL:"+afterL);

输出结果:

rankGreat95:{Deniro=96.5, Lily=91.1}
nameOrder:{Lily=91.1}

14、集合元素布尔判断

通过表达式 ![projectionExpression],我们可以判断集合中每一个元素是否符合表达式规则。

List list = (List) parser.parseExpression("{3,4,5}").getValue();
System.out.println("list:" + list);
   List<Boolean> isgreat4=(List<Boolean>)parser.parseExpression("![#this>3]")
        .getValue(list);
System.out.println("isgreat4:" + isgreat4);

输出结果:

isgreat4:[false, true, true]

也可以对 Map 对象进行类似判断。

15、实战经验

目前我们通过切面,拦截Controller方法,对于用户操作的日志做操作记录存储。我们对于日志区别(创建、修改、删除)操作,这时就意味着切入的方法(比如save方法适用于新增、修改)需要区分是满足什么条件是修改。

定义一个注解

/**
 * @description: 操作日志切面
 * @Date : 2019/5/31 下午5:35
 * @Author : 石冬冬-Seig Heil
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface OperateLogger {
    /**
     * 业务模块
     * @return
     */
    LogBizModuleEnum value();
    /**
     * 日志类型
     * @return
     */
    LogType logType() default LogType.U;
    /**
     * 关联ID
     * @return
     */
    String refId() default "";
    /**
     * 修改条件
     * @return
     */
    String modifyCondition() default "";
    /**
     * 日志类型
     */
    enum LogType{
        U,C,D
    }
}

定义一个切面

/**
 * @description: 用户操作日志切面
 * @Date : 2019/5/31 下午5:38
 * @Author : 石冬冬-Seig Heil
 */
@Component
@Aspect
@Slf4j
public class OperateLoggerAdvice {
    @Autowired
    UserLogService userLogService;

    @Pointcut("@annotation(com.mljr.transfer.aop.OperateLogger)")
    private void pointcut() {}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            Class<?> targetClass = joinPoint.getTarget().getClass();
            String className = targetClass.getName();
            String target = className + "." + method.getName();
            Object[] args = joinPoint.getArgs();
            OperateLogger annotation = method.getAnnotation(OperateLogger.class);
            String modifyCondition = annotation.modifyCondition();
            String authType = annotation.logType().name();
            String refId = "";
            boolean hasCondition = StringTools.isNotEmpty(modifyCondition);
            try {
                if(hasCondition){
                    if(!getExpression(args,modifyCondition,Boolean.class)){
                        authType =  OperateLogger.LogType.C.name();
                    }else{
                        authType =  OperateLogger.LogType.U.name();
                    }
                }else{
                    authType = StringTools.isEmpty(authType) ? OperateLogger.LogType.U.name() : authType;
                }
                refId = getExpression(args,annotation.refId(),Object.class).toString();
            }catch (Exception e){

            }
            Map<String,Object> logDetail = Maps.newHashMapWithExpectedSize(3);
            logDetail.put("target",target);
            logDetail.put("refId",refId);
            if(hasCondition){
                logDetail.put("modifyCondition",modifyCondition);
            }
            logDetail.put("params", getPointcutParams(joinPoint,method));
            TokenDto tokenDto = LoginToken.get();
            UserLog logEntity = UserLog.builder()
                    .authModel(annotation.value().getIndex())
                    .authType(authType)
                    .refId(refId)
                    .authDetail(JSONObject.toJSONString(logDetail))
                    .userId(Integer.valueOf(tokenDto.getSystemUserId()))
                    .userName(tokenDto.getUserName()).build();
            //这里需要根据header中来源手动切数据源
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            String systemSource = sra.getRequest().getHeader("systemSource");
            systemSource = Optional.ofNullable(systemSource).orElse(sra.getRequest().getHeader("systemsource"));
            DynamicDataSourceHandler.set(DataSourceEnum.getByName(systemSource));
            userLogService.insertRecord(logEntity);
        } catch (Exception e) {
            log.error("OperateLoggerAdvice save operate log exception",e);
        }
        return joinPoint.proceed();
    }

    /**
     * 获取切入点方法参数
     * @return
     */
    JSONObject getPointcutParams(ProceedingJoinPoint pjp, Method method){
        String[] pNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
        Object[] pValues = pjp.getArgs();
        JSONObject params = new JSONObject();
        if (ArrayUtils.isNotEmpty(pNames) && ArrayUtils.isNotEmpty(pValues) && pNames.length == pValues.length) {
            for (int i = 0; i < pValues.length; i++) {
                params.put(pNames[i], spotValue(pValues[i]));
            }
        }
        return params;
    }

    Object spotValue(Object object) {
        if (object instanceof HttpServletResponse) {
            return CollectionsTools.createMapNoCheck("response","response");
        } else if (object instanceof MultipartFile) {
            MultipartFile multipartFile = (MultipartFile) object;
            return CollectionsTools.createMapNoCheck("fileName",multipartFile.getOriginalFilename(),"size",multipartFile.getSize(),"contentType",multipartFile.getContentType());
        } else {
            return object;
        }
    }

    /**
     * 获取SPEL表达式值
     * @param args
     * @param expression
     * @param clazz
     * @param <T>
     * @return
     */
    <T> T getExpression(Object[] args,String expression,Class<T> clazz){
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context;
        if(args.length > 1){
            context = new StandardEvaluationContext(args);
        }else{
            Object obj = args[0];
            if(obj instanceof Integer || obj instanceof String || obj instanceof Long || obj instanceof Short){
                return parser.parseExpression(obj.toString()).getValue(clazz);
            }
            context = new StandardEvaluationContext(args[0]);
        }
        return parser.parseExpression(expression).getValue(context,clazz);
    }
}

请注意,文章来源:https://www.jianshu.com/p/5537b2c86acd
后面【实战经验】章节乃我所作,简述文章不错,看过后直接实践我的构思想法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值