MultiHashKey

This commit is contained in:
redkale
2024-01-30 20:52:54 +08:00
parent f725e491f0
commit c83aa7e8a8
13 changed files with 96 additions and 83 deletions

View File

@@ -11,8 +11,8 @@
| --- | --- | --- | | --- | --- | --- |
|key|未定义|缓存的key支持参数动态组合比如"key_#{id}"| |key|未定义|缓存的key支持参数动态组合比如"key_#{id}"|
|hash|```DEFAULT_HASH```|缓存的hash, 不能含有':'、'#'、'@'字符| |hash|```DEFAULT_HASH```|缓存的hash, 不能含有':'、'#'、'@'字符|
|localExpire|-1|本地缓存过期时长, 0表示永不过期 -1表示不作本地缓存。 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;5*60: 乘法表达式值为300 <br> &emsp;${env.cache.expires}: 读取系统配置项 <br> &emsp;#delays: 读取宿主对象的delays字段值作为值<br> &emsp;&emsp;&emsp;&emsp;&emsp; 字段类型必须是int、long数值类型 | |localExpire|-1|本地缓存过期时长, 0表示永不过期 -1表示不作本地缓存。 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;${env.cache.expires}: 读取系统配置项 |
|remoteExpire|-1|远程缓存过期时长, 0表示永不过期 -1表示不作远程缓存。 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;5*60: 乘法表达式值为300 <br> &emsp;${env.cache.expires}: 读取系统配置项 <br> &emsp;#delays: 读取宿主对象的delays字段值作为值<br> &emsp;&emsp;&emsp;&emsp;&emsp; 字段类型必须是int、long数值类型 | |remoteExpire|-1|远程缓存过期时长, 0表示永不过期 -1表示不作远程缓存。 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;${env.cache.expires}: 读取系统配置项 |
|nullable|false|是否可以缓存null值| |nullable|false|是否可以缓存null值|
|timeUnit|```TimeUnit.SECONDS```|时间单位TimeUnit| |timeUnit|```TimeUnit.SECONDS```|时间单位TimeUnit|
|comment|未定义|备注描述| |comment|未定义|备注描述|

View File

@@ -7,9 +7,9 @@
|name|未定义|名称, 可用于第三方实现的定时任务组件的key, 比如xxl-job的任务标识| |name|未定义|名称, 可用于第三方实现的定时任务组件的key, 比如xxl-job的任务标识|
|cron|未定义|cron表达式也可以使用常量值: <br> &emsp;@yearly、@annually、@monthly、@weekly、<br> &emsp;@daily、@midnight、@hourly、@minutely <br> &emsp;@1m、@2m、@3m、@5m、@10m、@15m、@30m <br> &emsp;@1h、@2h、@3h、@6h <br> &emsp;${env.scheduling.cron}: 读取系统配置项| |cron|未定义|cron表达式也可以使用常量值: <br> &emsp;@yearly、@annually、@monthly、@weekly、<br> &emsp;@daily、@midnight、@hourly、@minutely <br> &emsp;@1m、@2m、@3m、@5m、@10m、@15m、@30m <br> &emsp;@1h、@2h、@3h、@6h <br> &emsp;${env.scheduling.cron}: 读取系统配置项|
|zone|未定义|时区,```cron```有值才有效, 例如: "Asia/Shanghai"| |zone|未定义|时区,```cron```有值才有效, 例如: "Asia/Shanghai"|
|fixedDelay|-1|延迟时间,负数为无效值,支持参数配置、乘法表达式和对象字段值 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;5*60: 乘法表达式值为300 <br> &emsp;${env.scheduling.fixedDelay}: 读取系统配置项 <br> &emsp;#delays: 读取宿主对象的delays字段值作为值, <br> &emsp;&emsp;&emsp;&emsp; 字段类型必须是int、long数值类型 <br> 值大于0且fixedRate小于0则使用 ScheduledThreadPoolExecutor.scheduleWithFixedDelay | |fixedDelay|-1|延迟时间,负数为无效值,支持参数配置、乘法表达式和对象字段值 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;${env.scheduling.fixedDelay}: 读取系统配置项 <br> 值大于0且fixedRate小于0则使用 ScheduledThreadPoolExecutor.scheduleWithFixedDelay |
|fixedRate|-1|周期时间,负数为无效值,支持参数配置、乘法表达式和对象字段值 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;5*60: 乘法表达式值为300 <br> &emsp;${env.scheduling.fixedRate}: 读取系统配置项 <br> &emsp;#intervals: 读取宿主对象的intervals字段值作为值, <br> &emsp;&emsp;&emsp;&emsp;&emsp;&emsp; 字段类型必须是int、long数值类型 <br> 值大于0且fixedRate小于0则使用 ScheduledThreadPoolExecutor.scheduleAtFixedRate | |fixedRate|-1|周期时间,负数为无效值,支持参数配置、乘法表达式和对象字段值 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;${env.scheduling.fixedRate}: 读取系统配置项 <br> 值大于0且fixedRate小于0则使用 ScheduledThreadPoolExecutor.scheduleAtFixedRate |
|initialDelay|-1|起始延迟时间,负数为无效值,支持参数配置、乘法表达式和对象字段值 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;5*60: 乘法表达式值为300 <br> &emsp;${env.scheduling.initialDelay}: 读取系统配置项 <br> &emsp;#inits: 读取宿主对象的inits字段值作为值, <br> &emsp;&emsp;&emsp;&emsp;字段类型必须是int、long数值类型 <br> 值大于0且fixedRate和fixedDelay小于0则使用 ScheduledThreadPoolExecutor.schedule | |initialDelay|-1|起始延迟时间,负数为无效值,支持参数配置、乘法表达式和对象字段值 <br> 参数值支持方式:<br> &emsp;100: 设置数值 <br> &emsp;${env.scheduling.initialDelay}: 读取系统配置项 <br> 值大于0且fixedRate和fixedDelay小于0则使用 ScheduledThreadPoolExecutor.schedule |
|timeUnit|```TimeUnit.SECONDS```|时间单位TimeUnit| |timeUnit|```TimeUnit.SECONDS```|时间单位TimeUnit|
|comment|未定义|备注描述| |comment|未定义|备注描述|
|mode|```LoadMode.LOCAL```|作用于Service模式默认值为LOCAL<br> LOCAL: 表示远程模式的Service对象中的定时任务不起作用| |mode|```LoadMode.LOCAL```|作用于Service模式默认值为LOCAL<br> LOCAL: 表示远程模式的Service对象中的定时任务不起作用|
@@ -23,6 +23,14 @@
} }
``` ```
&emsp;&emsp;<b>数值配置</b>, 系统启动后延迟10分钟后每60分钟执行一次
```java
@Scheduled(fixedDelay = "10", fixedRate = "60", timeUnit = TimeUnit.MINUTES)
private void task3() {
System.out.println(Times.nowMillis() + "执行一次");
}
```
&emsp;&emsp;<b>环境配置</b>, 定时间隔时间由环境变量```env.schedule.fixedRate```配置没配置采用默认值60秒) &emsp;&emsp;<b>环境配置</b>, 定时间隔时间由环境变量```env.schedule.fixedRate```配置没配置采用默认值60秒)
```java ```java
@Scheduled(fixedRate = "${env.schedule.fixedRate:60}") @Scheduled(fixedRate = "${env.schedule.fixedRate:60}")
@@ -32,13 +40,6 @@
} }
``` ```
&emsp;&emsp;<b>支持乘法表达式</b>, 系统启动后延迟10分钟后每60分钟执行一次
```java
@Scheduled(fixedDelay = "10", fixedRate = "2*30", timeUnit = TimeUnit.MINUTES)
private void task3() {
System.out.println(Times.nowMillis() + "执行一次");
}
```
## 基本配置 ## 基本配置
```xml ```xml

View File

@@ -44,9 +44,7 @@ public @interface Cached {
* 本地缓存过期时长, 0表示永不过期 -1表示不作本地缓存。<br> * 本地缓存过期时长, 0表示永不过期 -1表示不作本地缓存。<br>
* 参数值支持方式:<br> * 参数值支持方式:<br>
* 100: 设置数值 * 100: 设置数值
* 5*60: 乘法表达式值为300
* ${env.cache.expires}: 读取系统配置项 * ${env.cache.expires}: 读取系统配置项
* #delays: 读取宿主对象的delays字段值作为值字段类型必须是int、long数值类型
* *
* @return 过期时长 * @return 过期时长
*/ */
@@ -56,9 +54,7 @@ public @interface Cached {
* 远程缓存过期时长, 0表示永不过期 -1表示不作远程缓存。<br> * 远程缓存过期时长, 0表示永不过期 -1表示不作远程缓存。<br>
* 参数值支持方式:<br> * 参数值支持方式:<br>
* 100: 设置数值 * 100: 设置数值
* 5*60: 乘法表达式值为300
* ${env.cache.expires}: 读取系统配置项 * ${env.cache.expires}: 读取系统配置项
* #delays: 读取宿主对象的delays字段值作为值字段类型必须是int、long数值类型
* *
* @return 过期时长 * @return 过期时长
*/ */

View File

@@ -13,6 +13,8 @@ import org.redkale.convert.json.JsonConvert;
* <p> * <p>
* 详情见: https://redkale.org * 详情见: https://redkale.org
* *
* @see org.redkale.mq.MessageConsumer
*
* @author zhangjx * @author zhangjx
* *
* @since 2.8.0 * @since 2.8.0

View File

@@ -3,10 +3,10 @@
*/ */
package org.redkale.mq; package org.redkale.mq;
import org.redkale.annotation.ClassDepends;
import org.redkale.annotation.Component; import org.redkale.annotation.Component;
import org.redkale.service.Local; import org.redkale.service.Local;
import org.redkale.util.AnyValue; import org.redkale.util.AnyValue;
import org.redkale.annotation.ClassDepends;
/** /**
* MQ消费器 实现类必须标记{@link org.redkale.mq.ResourceConsumer} * MQ消费器 实现类必须标记{@link org.redkale.mq.ResourceConsumer}
@@ -14,8 +14,11 @@ import org.redkale.annotation.ClassDepends;
* <p> * <p>
* 详情见: https://redkale.org * 详情见: https://redkale.org
* *
* @see org.redkale.mq.MessageConext
* @see org.redkale.mq.ResourceConsumer
*
* @author zhangjx * @author zhangjx
* @param <T> 泛型 * @param <T> T
* *
* @since 2.8.0 * @since 2.8.0
*/ */

View File

@@ -14,6 +14,8 @@ import org.redkale.convert.ConvertType;
* <p> * <p>
* 详情见: https://redkale.org * 详情见: https://redkale.org
* *
* @see org.redkale.mq.MessageProducer
*
* @author zhangjx * @author zhangjx
* *
* @since 2.8.0 * @since 2.8.0

View File

@@ -52,9 +52,7 @@ public @interface Scheduled {
* 延迟时间,支持参数配置、乘法表达式和对象字段值 <br> * 延迟时间,支持参数配置、乘法表达式和对象字段值 <br>
* 参数值支持方式:<br> * 参数值支持方式:<br>
* 100: 设置数值 * 100: 设置数值
* 5*60: 乘法表达式值为300
* ${env.scheduling.fixedDelay}: 读取系统配置项 * ${env.scheduling.fixedDelay}: 读取系统配置项
* #delays: 读取宿主对象的delays字段值作为值字段类型必须是int、long数值类型
* *
* 值大于0且fixedRate小于0则使用 ScheduledThreadPoolExecutor.scheduleWithFixedDelay * 值大于0且fixedRate小于0则使用 ScheduledThreadPoolExecutor.scheduleWithFixedDelay
* *
@@ -66,9 +64,7 @@ public @interface Scheduled {
* 周期时间,支持参数配置、乘法表达式和对象字段值 <br> * 周期时间,支持参数配置、乘法表达式和对象字段值 <br>
* 参数值支持方式:<br> * 参数值支持方式:<br>
* 100: 设置数值 * 100: 设置数值
* 5*60: 乘法表达式值为300
* ${env.scheduling.fixedRate}: 读取系统配置项 * ${env.scheduling.fixedRate}: 读取系统配置项
* #intervals: 读取宿主对象的intervals字段值作为值字段类型必须是int、long数值类型
* *
* 值大于0则使用 ScheduledThreadPoolExecutor.scheduleAtFixedRate * 值大于0则使用 ScheduledThreadPoolExecutor.scheduleAtFixedRate
* *
@@ -80,9 +76,7 @@ public @interface Scheduled {
* 起始延迟时间,支持参数配置、乘法表达式和对象字段值 <br> * 起始延迟时间,支持参数配置、乘法表达式和对象字段值 <br>
* 参数值支持方式:<br> * 参数值支持方式:<br>
* 100: 设置数值 * 100: 设置数值
* 5*60: 乘法表达式值为300
* ${env.scheduling.initialDelay}: 读取系统配置项 * ${env.scheduling.initialDelay}: 读取系统配置项
* #inits: 读取宿主对象的inits字段值作为值字段类型必须是int、long数值类型
* *
* 值大于0且fixedRate和fixedDelay小于0则使用 ScheduledThreadPoolExecutor.schedule * 值大于0且fixedRate和fixedDelay小于0则使用 ScheduledThreadPoolExecutor.schedule
* *

View File

@@ -6,7 +6,6 @@ package org.redkale.schedule.spi;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.time.Duration; import java.time.Duration;
@@ -221,9 +220,9 @@ public class ScheduleManagerService implements ScheduleManager, Service {
CronExpression cronExpr = CronExpression.parse(cron); CronExpression cronExpr = CronExpression.parse(cron);
return new CronTask(ref, name, method, cronExpr, zoneId); return new CronTask(ref, name, method, cronExpr, zoneId);
} else { } else {
long fixedDelayLong = getLongValue(ref.get(), fixedDelay); long fixedDelayLong = Long.parseLong(fixedDelay);
long fixedRateLong = getLongValue(ref.get(), fixedRate); long fixedRateLong = Long.parseLong(fixedRate);
long initialDelayLong = getLongValue(ref.get(), initialDelay); long initialDelayLong = Long.parseLong(initialDelay);
return new FixedTask(ref, name, method, fixedDelayLong, fixedRateLong, initialDelayLong, timeUnit); return new FixedTask(ref, name, method, fixedDelayLong, fixedRateLong, initialDelayLong, timeUnit);
} }
} }
@@ -270,46 +269,6 @@ public class ScheduleManagerService implements ScheduleManager, Service {
return propertyFunc.apply(value); return propertyFunc.apply(value);
} }
//支持5*60乘法表达式
protected long getLongValue(Object service, String value) {
if (value.indexOf('*') > -1) {
long rs = 1;
boolean flag = false;
for (String v : value.split("\\*")) {
if (!v.trim().isEmpty()) {
rs *= Long.parseLong(v.trim());
flag = true;
}
}
return flag ? rs : -1;
} else if (value.indexOf('#') == 0) {
try {
String fieldName = value.substring(1);
Exception ex = null;
Field field = null;
Class clazz = service.getClass();
do {
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
RedkaleClassLoader.putReflectionField(clazz.getName(), field);
break;
} catch (NoSuchFieldException fe) {
ex = fe;
}
} while ((clazz = clazz.getSuperclass()) != Object.class);
if (field == null) {
throw ex;
}
return ((Number) field.get(service)).longValue();
} catch (Exception e) {
throw new RedkaleException(e);
}
} else {
return Long.parseLong(value);
}
}
@Override @Override
public int start(String scheduleName) { public int start(String scheduleName) {
int c = 0; int c = 0;

View File

@@ -164,7 +164,7 @@ public interface DataSource extends Resourcable {
//-------------------------deleteAsync-------------------------- //-------------------------deleteAsync--------------------------
/** /**
* 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br> * 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br>
等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br> * 等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br>
* *
* @param <T> 泛型 * @param <T> 泛型
* @param entitys Entity对象 * @param entitys Entity对象
@@ -175,7 +175,7 @@ public interface DataSource extends Resourcable {
/** /**
* 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br> * 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br>
等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br> * 等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br>
* *
* @param <T> 泛型 * @param <T> 泛型
* @param entitys Entity对象 * @param entitys Entity对象
@@ -191,7 +191,7 @@ public interface DataSource extends Resourcable {
/** /**
* 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br> * 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br>
等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br> * 等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br>
* *
* @param <T> 泛型 * @param <T> 泛型
* @param entitys Entity对象 * @param entitys Entity对象
@@ -207,7 +207,7 @@ public interface DataSource extends Resourcable {
/** /**
* 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br> * 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br>
等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br> * 等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br>
* *
* @param <T> 泛型 * @param <T> 泛型
* @param entitys Entity对象 * @param entitys Entity对象
@@ -218,7 +218,7 @@ public interface DataSource extends Resourcable {
/** /**
* 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br> * 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br>
等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br> * 等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br>
* *
* @param <T> 泛型 * @param <T> 泛型
* @param entitys Entity对象 * @param entitys Entity对象
@@ -234,7 +234,7 @@ public interface DataSource extends Resourcable {
/** /**
* 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br> * 删除指定主键值的记录, 多对象必须是同一个Entity类且必须在同一张表中 <br>
等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br> * 等价SQL: DELETE FROM {table} WHERE {primary} IN {getValues.id} <br>
* *
* @param <T> 泛型 * @param <T> 泛型
* @param entitys Entity对象 * @param entitys Entity对象

View File

@@ -60,11 +60,25 @@ public class Environment implements java.io.Serializable {
if (val == null || val.isBlank()) { if (val == null || val.isBlank()) {
return val; return val;
} }
char last = 0;
char[] chars = val.toCharArray();
int startIndex = -1;
int endIndex = -1;
for (int i = 0; i < chars.length; i++) {
char ch = chars[i];
if (ch == '{' && last == '$') {
startIndex = i - 1;
} else if (last != '\\' && ch == '}' && startIndex >= 0) {
endIndex = i;
break;
}
last = ch;
}
//${domain}/${path}/xxx ${aa${bbb}} //${domain}/${path}/xxx ${aa${bbb}}
int pos2 = val.indexOf("}"); //school_#{name}_${haha_${age}}_${bb}_#{dd} -> school_#{name}_xxx_xxx_#{dd}
int pos1 = val.lastIndexOf("${", pos2); if (startIndex >= 0 && endIndex > 0) {
if (pos1 >= 0 && pos2 > 0) { String key = val.substring(startIndex + 2, endIndex);
String key = val.substring(pos1 + 2, pos2);
int pos3 = key.lastIndexOf(':'); int pos3 = key.lastIndexOf(':');
String defVal = null; String defVal = null;
if (pos3 > 0) { if (pos3 > 0) {
@@ -77,13 +91,13 @@ public class Environment implements java.io.Serializable {
String subVal = properties.getProperty(key); String subVal = properties.getProperty(key);
if (subVal != null) { if (subVal != null) {
String newVal = getPropertyValue(subVal, envs); String newVal = getPropertyValue(subVal, envs);
return getPropertyValue(val.substring(0, pos1) + newVal + val.substring(pos2 + 1)); return getPropertyValue(val.substring(0, startIndex) + newVal + val.substring(endIndex + 1));
} else { } else {
for (Properties prop : envs) { for (Properties prop : envs) {
subVal = prop.getProperty(key); subVal = prop.getProperty(key);
if (subVal != null) { if (subVal != null) {
String newVal = getPropertyValue(subVal, envs); String newVal = getPropertyValue(subVal, envs);
return getPropertyValue(val.substring(0, pos1) + newVal + val.substring(pos2 + 1)); return getPropertyValue(val.substring(0, startIndex) + newVal + val.substring(endIndex + 1));
} }
} }
if (pos3 > 0) { if (pos3 > 0) {
@@ -91,8 +105,6 @@ public class Environment implements java.io.Serializable {
} }
throw new RedkaleException("Not found '" + key + "' value"); throw new RedkaleException("Not found '" + key + "' value");
} }
} else if ((pos1 >= 0 && pos2 < 0) || (pos1 < 0 && pos2 >= 0 && val.indexOf("#{") < 0)) {
throw new RedkaleException(val + " is illegal naming");
} }
return val; return val;
} }

View File

@@ -19,7 +19,7 @@ public interface MultiHashKey {
public String keyFor(Object... args); public String keyFor(Object... args);
/** /**
* key只支持带#{}的表达式, 且不能嵌套,:name_#{key_#{id}} * key只支持带#{}的表达式, 且不能嵌套,错误示例:name_#{key_#{id}}
* *
* @param paramNames 参数名 * @param paramNames 参数名
* @param key key表达式 * @param key key表达式

View File

@@ -39,9 +39,15 @@ class MultiHashKeys {
} }
sb.delete(0, sb.length()); sb.delete(0, sb.length());
paraming = true; paraming = true;
} else if (last == '\\') {
sb.deleteCharAt(sb.length() - 1);
sb.append(ch);
} else { } else {
throw new RedkaleException(MultiHashKey.class.getSimpleName() + " parse error, key: " + key); throw new RedkaleException(MultiHashKey.class.getSimpleName() + " parse error, key: " + key);
} }
} else if (last == '\\' && ch == '}') {
sb.deleteCharAt(sb.length() - 1);
sb.append(ch);
} else if (ch == '}') { } else if (ch == '}') {
if (!paraming) { if (!paraming) {
throw new RedkaleException(MultiHashKey.class.getSimpleName() + " parse error, key: " + key); throw new RedkaleException(MultiHashKey.class.getSimpleName() + " parse error, key: " + key);

View File

@@ -0,0 +1,38 @@
/*
*
*/
package org.redkale.test.util;
import java.util.Properties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.redkale.util.Environment;
/**
*
* @author zhangjx
*/
public class EnvironmentTest {
public static void main(String[] args) throws Throwable {
EnvironmentTest test = new EnvironmentTest();
test.run1();
}
@Test
public void run1() throws Exception {
Properties properties = new Properties();
properties.put("age", "18");
properties.put("haha_18", "test");
properties.put("bb", "tt");
Environment env = new Environment(properties);
String val = env.getPropertyValue("school_#{name}_${haha_${age}}_${bb}_#{dd}");
System.out.println(val);
Assertions.assertEquals("school_#{name}_test_tt_#{dd}", val);
val = env.getPropertyValue("${haha_${age}}");
System.out.println(val);
Assertions.assertEquals("test", val);
}
}