作为一名专门写bug的Java程序猿,相信大家都会遇到过这样的问题:项目的业务逻辑很复杂,而且还经常变化,今天的一个办理条件是小于5,明天就变成了大于10或者条件作废。这就很头疼了,里面的数字可以抽取到配置文件,但是大于和小于呢?条件作废呢?
对于业务规则的复杂性,我们可以使用一些规则引擎来解决代码可读性差的问题。市面上也有不少的规则引擎框架,开源的不开源的,收费的不收费的,我们这里推荐使用的是EasyRules(https://github.com/j-easy/easy-rules)。
对于业务规则的变化性,部分变量的值可以抽取出来放到配置文件里面。但是大部分的需求变化,可不是改变一下变量的值那么简单,可能是一大段代码的重写,这就需要利用Java6之后提供的动态编译来实现了。
废话不多说,精彩马上来!
思路:在EasyRules中,一个if (...) {...}对应一条规则,也对应着一个类。这样我们可以将这个类的信息(源码、编译后字节码、类名、所属分组等)存到数据库,以提供系统在运行时修改源码、重新编译、动态加载、替换规则的功能。
具体实现:定义规则类,这个类除了有EasyRule的类名、源码、编译后字节码等信息之外,还有一些其它属性,比如规则所属分组、执行优先级、启动状态等。当我们在页面新增(或者修改)了源码,提交之后对其进行编译,将得到类名和字节码,然后将这些数据保存到数据库。如果规则是启用状态,还要创建一个实例存放到到我们维护的一个map集合里(如果存在同类名的实例就替换),以供规则引擎去调用。
首先EasyRule是一个规则引擎.这个名字由来是受到了Martin Fowler 的文章 Should I use a Rules Engine
You can build a simple rules engine yourself. All you need is to create a bunch of objects with conditions and actions, store them in a collection, and run through them to evaluate the conditions and execute the actions.
Useful abstractions to define business rules and apply them easily with Java
The ability to create composite rules from primitive ones
1. First, define your rule..
Either in a declarative way using annotations:
@Rule(name = "weather rule", description = "if it rains then take an umbrella" )
public class WeatherRule {
@Condition
public boolean itRains(@Fact("rain") boolean rain) {
return rain;
}
@Action
public void takeAnUmbrella() {
System.out.println("It rains, take an umbrella!");
}
}
Or in a programmatic way with a fluent API:
Rule weatherRule = new RuleBuilder()
.name("weather rule")
.description("if it rains then take an umbrella")
.when(facts -> facts.get("rain").equals(true))
.then(facts -> System.out.println("It rains, take an umbrella!"))
.build();
Or using an Expression Language:
Rule weatherRule = new MVELRule()
.name("weather rule")
.description("if it rains then take an umbrella")
.when("rain == true")
.then("System.out.println(\"It rains, take an umbrella!\");");
Or using a rule descriptor:
Like in the following weather-rule.yml
example file:
name: "weather rule"
description: "if it rains then take an umbrella"
condition: "rain == true"
actions:
- "System.out.println(\"It rains, take an umbrella!\");"
Rule weatherRule = MVELRuleFactory.createRuleFrom(new File("weather-rule.yml"));
2. Then, fire it!
public class Test {
public static void main(String[] args) {
// define facts
Facts facts = new Facts();
facts.put("rain", true);
// define rules
Rule weatherRule = ...
Rules rules = new Rules();
rules.register(weatherRule);
// fire rules on known facts
RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, facts);
}
}
上面例子没有提到的是,规则类可以设置执行优先级,具体做法就是在类里面定义一个返回类型是int的方法,然后在方法上面加一个注解@Priority。
我们将类名叫做JavaRuleDO,所有属性都对应着数据库JAVA_RULE表的字段。
这里使用了lombok插件,因此可以省略了getter、setter和toString方法,还有其它注解,挺好用的,感兴趣的童鞋可以去看看。
@Getter
@Setter
@ToString
@Entity
@Table(name = "JAVA_RULE")
public class JavaRuleDO implements Serializable {
private static final long serialVersionUID = 830103606495004702L;
@Id
private Long id;
// 目标,一般指哪个系统
@Column
private String target;
// 文件名
@Column
private String fileName;
// 全类名
@Column
private String fullClassName;
// 类名
@Column
private String simpleClassName;
// 源码
@Column
private String srcCode;
// 编译后字节码
@Column
private byte[] byteContent;
// 创建时间
@Column
private Date createTime;
// 创建用户id
@Column
private Long createUserId = Consts.Entity.NULL_ID_PLACEHOLDER;
// 创建用户名称
@Column
private String createUserName;
// 更新时间
@Column
private Date updateTime;
// 更新用户id
@Column
private Long updateUserId = Consts.Entity.NULL_ID_PLACEHOLDER;
// 更新用户名称
@Column
private String updateUserName;
// 是否已删除,1是 0否
@Column
private Integer isDeleted = Consts.Entity.NOT_DELETED;
// 状态,1有效 0无效
@Column
private Integer status = Consts.Entity.NOT_VALID;
// 组别名称,一般指哪一系列规则
@Column
private String groupName;
// 顺序(优先级)
@Column
private Integer sort = Integer.MAX_VALUE;
// 规则名称
@Column
private String name;
// 规则描述
@Column
private String description;
}
下面附上Oracle版本的建表SQL。
create table JAVA_RULE
(
id NUMBER(11) not null,
target VARCHAR2(32) not null,
file_name VARCHAR2(32) not null,
full_class_name VARCHAR2(64) not null,
simple_class_name VARCHAR2(32) not null,
src_code CLOB not null,
byte_content BLOB not null,
create_time DATE not null,
create_user_id NUMBER(11) not null,
create_user_name VARCHAR2(128) not null,
update_time DATE,
update_user_id NUMBER(11),
update_user_name VARCHAR2(128),
is_deleted NUMBER(1) default 0 not null,
status NUMBER(1) default 1 not null,
group_name VARCHAR2(32) not null,
sort NUMBER(3) default 999,
name VARCHAR2(512) not null,
description VARCHAR2(2048)
)
;
comment on column JAVA_RULE.id
is '主键';
comment on column JAVA_RULE.target
is '目标,一般指哪个系统';
comment on column JAVA_RULE.file_name
is '文件名';
comment on column JAVA_RULE.full_class_name
is '全类名';
comment on column JAVA_RULE.simple_class_name
is '类名';
comment on column JAVA_RULE.src_code
is '源码';
comment on column JAVA_RULE.byte_content
is '编译后字节码';
comment on column JAVA_RULE.create_time
is '创建时间';
comment on column JAVA_RULE.create_user_id
is '创建用户id';
comment on column JAVA_RULE.create_user_name
is '创建用户名称';
comment on column JAVA_RULE.update_time
is '更新时间';
comment on column JAVA_RULE.update_user_id
is '更新用户id';
comment on column JAVA_RULE.update_user_name
is '更新用户名称';
comment on column JAVA_RULE.is_deleted
is '是否已删除,1是 0否';
comment on column JAVA_RULE.status
is '状态,1有效 0无效';
comment on column JAVA_RULE.group_name
is '组别名称,一般指哪一系列规则';
comment on column JAVA_RULE.sort
is '顺序(优先级)';
comment on column JAVA_RULE.name
is '规则名称';
comment on column JAVA_RULE.description
is '规则描述';
create unique index IDX_JAVA_RULE_FULL_CLASS_NAME on JAVA_RULE (FULL_CLASS_NAME);
alter table JAVA_RULE
add constraint PK_JAVA_RULE primary key (ID);
动态编译一直是Java的梦想,从Java 6版本它开始支持动态编译了,可以在运行期直接编译.java文件,执行.class。但是还要慎用,因为存在性能和安全问题。
下面提供了一个编译源码的工具方法。如果编译成功,返回一个CompileResult对象(自定义类型,下面给出了源码);如果编译失败,返回具体的编译不通过原因。其中,Result是贵公司封装的一个通用返回值包装器,这里不方便提供源码,抱歉。
代码1:动态编译源码的工具方法(想了解更多细节可以去参考其它资料),暂时先去掉了后面会用到的其它工具方法。
/**
* 规则工具类
* @author z_hh
* @date 2018年12月7日
*/
@Slf4j
public class DynamicRuleUtils {
// 编译版本
private static final String TARGET_CLASS_VERSION = "1.8";
/**
* auto fill in the java-name with code, return null if cannot find the public class
*
* @param javaSrc source code string
* @return return the Map, the KEY means ClassName, the VALUE means bytecode.
* @throws RuntimeException
*/
public static Result compile(String javaSrc) throws RuntimeException {
Pattern pattern = Pattern.compile("public\\s+class\\s+(\\w+)");
Matcher matcher = pattern.matcher(javaSrc);
if (matcher.find()) {
return compile(matcher.group(1) + ".java", javaSrc);
}
return Results.error("找不到类名称!");
}
/**
* @param javaName the name of your public class,eg: TestClass.java
* @param javaSrc source code string
* @return return the Map, the KEY means ClassName, the VALUE means bytecode.
* @throws RuntimeException
*/
public static Result compile(String javaName, String javaSrc) throws RuntimeException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = compiler.getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = MemoryJavaFileManager.makeStringSource(javaName, javaSrc);
DiagnosticCollector collector = new DiagnosticCollector<>();
List options = new ArrayList<>();
options.add("-target");
options.add(TARGET_CLASS_VERSION);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, collector, options, null,
Arrays.asList(javaFileObject));
if (task.call()) {
return Results.success(CompileResult.builder()
.mainClassFileName(javaName)
.byteCode(manager.getClassBytes())
.build());
}
String errorMessage = collector.getDiagnostics().stream()
.map(diagnostics -> diagnostics.toString())
.reduce("", (s1, s2) -> s1 + "\n" +s2);
return Results.error(errorMessage);
} catch (IOException e) {
log.error("编译出错啦!", e);
return Results.error(e.getMessage());
}
}
}
/**
* JavaFileManager that keeps compiled .class bytes in memory.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
final class MemoryJavaFileManager extends ForwardingJavaFileManager {
/**
* Java source file extension.
*/
private final static String EXT = ".java";
private Map classBytes;
public MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
classBytes = new HashMap();
}
public Map getClassBytes() {
return classBytes;
}
@Override
public void close() throws IOException {
classBytes = new HashMap();
}
@Override
public void flush() throws IOException {
}
/**
* A file object used to represent Java source coming from a string.
*/
private static class StringInputBuffer extends SimpleJavaFileObject {
final String code;
StringInputBuffer(String name, String code) {
super(toURI(name), Kind.SOURCE);
this.code = code;
}
public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(code);
}
@SuppressWarnings("unused")
public Reader openReader() {
return new StringReader(code);
}
}
/**
* A file object that stores Java bytecode into the classBytes map.
*/
private class ClassOutputBuffer extends SimpleJavaFileObject {
private String name;
ClassOutputBuffer(String name) {
super(toURI(name), Kind.CLASS);
this.name = name;
}
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
classBytes.put(name, bos.toByteArray());
}
};
}
}
@Override
public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) throws IOException {
if (kind == JavaFileObject.Kind.CLASS) {
return new ClassOutputBuffer(className);
} else {
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
static JavaFileObject makeStringSource(String name, String code) {
return new StringInputBuffer(name, code);
}
static URI toURI(String name) {
File file = new File(name);
if (file.exists()) {
return file.toURI();
} else {
try {
final StringBuilder newUri = new StringBuilder();
newUri.append("mfm:///");
newUri.append(name.replace('.', '/'));
if (name.endsWith(EXT))
newUri.replace(newUri.length() - EXT.length(), newUri.length(), EXT);
return URI.create(newUri.toString());
} catch (Exception exp) {
return URI.create("mfm:///com/sun/script/java/java_source");
}
}
}
}
代码2: CompileResult类,之所以设置mainClassFileName,是因为我们暂时不支持内部类,只保存顶级类的字节码。
/**
* 类编译结果
* @author z_hh
* @date 2018年12月5日
*/
@Getter
@Setter
@Builder
@ToString
public class CompileResult {
// 主类全类名
private String mainClassFileName;
// 编译出来的全类名和对应class字节码
private Map byteCode;
}
代码3:将编译得到的信息设置到实体对象。
// 编译
private Result compiler(JavaRuleDO entity) {
Result> result = DynamicRuleUtils.compile(entity.getSrcCode());
if (result.isError()) {
return (Result) result;
}
CompileResult compileResult = (CompileResult) result.get();
for (String classFullName : compileResult.getByteCode().keySet()) {
int lastIndex = classFullName.lastIndexOf(".");
String simpleName = lastIndex != -1 ? classFullName.substring(lastIndex + 1) : classFullName,
fileName = compileResult.getMainClassFileName();
// 只要最外层的类
if (fileName.startsWith(simpleName)) {
entity.setFileName(fileName);
entity.setFullClassName(classFullName);
entity.setSimpleClassName(simpleName);
entity.setByteContent(compileResult.getByteCode().get(classFullName));
return Results.success(entity);
}
}
return Results.error("没有找到最外层类!");
}
在动态编译阶段,我们已经得到了源码对应的字节码,这样就可以将其加载到JVM里面了。
在这里有必要提两点:第一,在Java虚拟机层面,相同的一个类,除了有相同的全类名以外,还要由相同的类加载器进行加载;第二,类加载的双亲委派模型,工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
因此,我们需要定义自己的类加载器,每次将class字节码加载到JVM都需要创建一个新的类加载器对象。主要是重写findClass方法,将我们传进去的字节码数组进行加载。
/*
* a temp memory class loader
*/
private static class MemoryClassLoader extends URLClassLoader {
Map classBytes = new HashMap();
public MemoryClassLoader(Map classBytes) {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
this.classBytes.putAll(classBytes);
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}
具体使用方式。
/**
* 从JavaRuleDO获取规则Class对象
* @param javaRule
* @throws Exception
*/
public static Class> getRuleClass(JavaRuleDO javaRule) throws Exception {
Map bytecode = new HashMap<>();
String fullName = Objects.requireNonNull(javaRule.getFullClassName(), "全类名不能为空!");
bytecode.put(fullName, javaRule.getByteContent());
try (MemoryClassLoader classLoader = new MemoryClassLoader(bytecode)) {
return classLoader.findClass(fullName);
} catch (Exception e) {
log.error("加载类{}异常!", fullName);
throw e;
}
}
这里强制所有规则类都要继承我们定义好的规则基类,有两个原因:
1、定义优先级属性,并提供set方法供外部设值、get方法供规则引擎获取。
2、重写hashCode方法和equals方法,以让容器(HashSet)认定相同类型的两个元素是相同的。
/**
* 规则基类
* @author z_hh
* @date 2018年12月12日
*/
public class BaseRule {
private int priority = Integer.MAX_VALUE;
/*重写equals方法和hashCode方法,让Set集合判定同类型的两个对象相同*/
@Override
public boolean equals(Object obj) {
return Objects.nonNull(obj)
&& Objects.equals(this.getClass().getName(), obj.getClass().getName());
}
@Override
public int hashCode() {
return Objects.hashCode(this.getClass().getName());
}
/**
* 获取优先级
*/
@Priority
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
}
首先我们定义一个容器接口,声明一些需要提供的接口。毕竟面向接口编程,方便以后扩展容器类型。
/**
* Java规则类存储器
* @author z_hh
* @date 2018年12月12日
*/
public interface JavaRuleStorage {
/**
* 容器是否包含指定规则
* @param javaRule
* @return
*/
boolean contains(String groupName, BaseRule rule);
/**
* 添加规则到容器
* @param javaRule
*/
boolean add(String groupName, BaseRule rule);
/**
* 批量添加规则到容器的指定组
* @param javaRules
*/
boolean batchAdd(String groupName, Iterable extends BaseRule> rules);
/**
* 从容器移除指定规则
* @param group
*/
boolean remove(String groupName, BaseRule rule);
/**
* 从容器移除指定组的规则
* @param group
*/
boolean remove(String group);
/**
* 从容器获取指定组的所有规则
* @param group
* @return
*/
Collection listObjByGroup(String group);
}
然后提供一个map实现版。或者使用Spring IOC容器,但我感觉没有那么灵活。这里的Multimap是Google Guava提供的集合类工具,类似于JDK里面的Map
/**
* Java规则类存储器Map版
* @author z_hh
* @date 2018年12月12日
*/
public class MapJavaRuleStorage implements JavaRuleStorage {
private final Multimap map = HashMultimap.create();
@Override
public boolean contains(String groupName, BaseRule rule) {
return map.containsEntry(groupName, rule);
}
@Override
public boolean add(String groupName, BaseRule rule) {
// 如果原来有,就先删除掉
if (map.containsEntry(groupName, rule)) {
map.remove(groupName, rule);
}
return map.put(groupName, rule);
}
@Override
public boolean batchAdd(String groupName, Iterable extends BaseRule> rules) {
return map.putAll(groupName, rules);
}
@Override
public boolean remove(String groupName, BaseRule rule) {
return map.remove(groupName, rule);
}
@Override
public boolean remove(String group) {
return !map.removeAll(group).isEmpty();
}
@Override
public Collection listObjByGroup(String group) {
return map.get(group);
}
}
获取规则实例的工具方法(其中getRuleClass(...)方法上面已提供)。
/**
* 从JavaRuleDO获取规则实例对象
* @param javaRule
* @throws Exception
*/
public static BaseRule getRuleInstance(JavaRuleDO javaRule) throws Exception {
try {
BaseRule rule = (BaseRule) getRuleClass(javaRule).newInstance();
// 设置优先级
rule.setPriority(javaRule.getSort());
return rule;
} catch (Exception e) {
log.error("从JavaRuleDO获取规则实例异常!", e);
throw e;
}
}
添加规则到容器,其中entity为JavaRuleDO实例。
/**
* 添加规则到容器
* @param entity
* @return
*/
private Result addRuleToStorage(JavaRuleDO entity) {
try {
BaseRule rule = DynamicRuleUtils.getRuleInstance(entity);
return javaRuleStorage.add(entity.getGroupName(), rule) ? Results.success(entity)
: Results.error("添加规则到容器失败!");
} catch (Exception e) {
log.error("添加规则{}到容器异常!", entity.getName(), e);
return Results.error("添加规则到容器异常!");
}
}
将规则从容器移除,其中entity为JavaRuleDO实例。
String groupName = entity.getGroupName();
try {
BaseRule rule = DynamicRuleUtils.getRuleInstance(entity);
if (javaRuleStorage.contains(groupName, rule) && !javaRuleStorage.remove(groupName, rule)) {
return Results.error("从容器移除规则失败!");
}
} catch (Exception e) {
log.error("从容器移除规则{}异常!", entity.getName(), e);
return Results.error("从容器移除规则异常!");
}
为了更方便使用规则引擎,我们特意创建了一个DynamicRuleManager,以实现链式调用。
/**
* 动态规则管理器
* @author z_hh
* @date 2018年12月12日
*/
@Component("dynamicRuleManager")
public class DynamicRuleManager {
public Builder builder() {
return new Builder(this);
}
public class Builder {
private Rules rules = new Rules();
private Facts facts = new Facts();
private RulesEngine engine = new DefaultRulesEngine();
private JavaRuleStorage javaRuleStorage;
public Builder(DynamicRuleManager dynamicRuleManager) {
javaRuleStorage = dynamicRuleManager.javaRuleStorage;
}
/**
* 设置参数,该参数为值传递,在规则里面或者执行完之后可以取到
* @param name
* @param value
* @return
*/
public Builder setParameter(String name, Object value) {
facts.put(name, value);
return this;
}
/**
* 增加规则组(将指定所属分组的所有启用规则添加进来)
* @param groupName
* @return
*/
public Builder addRuleGroup(String groupName) {
Collection rs = javaRuleStorage.listObjByGroup(groupName);
rs.stream().forEach(rules::register);
return this;
}
/**
* 运行规则引擎
*/
public Builder run() {
engine.fire(rules, facts);
return this;
}
/**
* 获取指定参数,并转为指定类型
* @param pName
* @param pType
* @return
*/
public T getParameter(String pName, Class pType) {
return facts.get(pName);
}
}
@Autowired
private JavaRuleStorage javaRuleStorage;
}
@Configuration
public class RuleDefaultConf {
@Bean
@ConditionalOnMissingBean
public JavaRuleStorage javaRuleStorage() {
return new MapJavaRuleStorage();
}
}
1、创建规则。
@Rule
public class DemoRule1 extends BaseRule {
@Condition
public boolean when(@Fact("param1") String param1) {
System.out.println("我是参数1,value=" + param1);
return true;
}
@Action
public void then(@Fact("param2") String param2) {
System.out.println("我是参数2,value=" + param2);
}
}
2、调用规则。
Builder builder = dynamicRuleManager.builder()
.setParameter("param1", "Hello")
.setParameter("param2", "World")
.addRuleGroup("testRule")
.run();
String param1 = builder.getParameter("param1", String.class);
String param2 = builder.getParameter("param2", String.class);
System.out.println(param1 + " " + param2);
3、执行结果。
我们设置了dynamic.rule.target(目标,所属系统)参数从配置文件获取。
/**
* 应用启动监听器
* @author z_hh
* @date 2018年12月10日
*/
@Slf4j
@WebListener
public class AppRunListener implements ServletContextListener {
@Value("${dynamic.rule.target}")
private String ruleTarget;
/**
* 将指定组的javaRule对象装进容器
*/
@Override
public void contextInitialized(ServletContextEvent event) {
if (StringUtils2.notEmpty(ruleTarget)) {
List javaRules = javaRuleService.createJpaQuery()
.where("status", Consts.Entity.IS_VALID)
.where("target", SqlFieldOperatorEnum.IN, Arrays.asList(ruleTarget.split(",")))
.list();
javaRules.stream()
.forEach(javaRule -> {
try {
BaseRule rule = DynamicRuleUtils.getRuleInstance(javaRule);
if (!javaRuleStorage.add(javaRule.getGroupName(), rule)) {
log.warn("添加规则{}到容器失败!", javaRule.getName());
javaRule.setStatus(Consts.Entity.NOT_VALID);
javaRuleService.save(javaRule);
}
log.info("添加了规则{}到容器", javaRule.getFullClassName());
} catch (Exception e) {
log.warn("添加规则{}到容器异常!", javaRule.getName());
javaRule.setStatus(Consts.Entity.NOT_VALID);
javaRuleService.save(javaRule);
}
});
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {
//
}
@Autowired
private JavaRuleService javaRuleService;
@Autowired
private JavaRuleStorage javaRuleStorage;
}
1、当我们在管理页面新增或者修改规则时,如果状态为启用,后台应该需要在编译之后创建规则实例并放进容器;反之,如果状态为禁用,后台就判断容器里是否有该规则类的实例,有的话需要将其移除。
2、管理页面的启用和禁用,对应着这个规则类实例添加到容器和从容器里面移除的操作。
3、删除规则时,如果启用状态不能删除,需要先将其禁用。
博文内容没有将涉及的代码全部展示。完整的代码已经打包上传到我的资源,大家可以去下载参考;还有,部分代码涉及到公司的框架,并没有包含在里面,可以换一种自己的方式实现的,抱歉。