From af6c46743f9bc6786e0ed529450e7ea2e4dda5dc Mon Sep 17 00:00:00 2001 From: xuxueli <931591021@qq.com> Date: Thu, 1 Nov 2018 09:21:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=97=A0=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=99=A8Sample=E7=A4=BA=E4=BE=8B=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=20"xxl-job-executor-sample-frameless"=E3=80=82?= =?UTF-8?q?=E4=B8=8D=E4=BE=9D=E8=B5=96=E7=AC=AC=E4=B8=89=E6=96=B9=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=EF=BC=8C=E5=8F=AA=E9=9C=80main=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF=E5=90=AF=E5=8A=A8=E8=BF=90=E8=A1=8C=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=99=A8=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/XXL-JOB官方文档.md | 15 +-- .../xxl/job/core/executor/XxlJobExecutor.java | 49 ++++++---- xxl-job-executor-samples/pom.xml | 1 + .../xxl-job-executor-sample-frameless/pom.xml | 37 +++++++ .../sample/frameless/Application.java | 33 +++++++ .../config/FrameLessXxlJobConfig.java | 96 +++++++++++++++++++ .../frameless/jobhandler/DemoJobHandler.java | 32 +++++++ .../frameless/jobhandler/HttpJobHandler.java | 63 ++++++++++++ .../jobhandler/ShardingJobHandler.java | 34 +++++++ .../src/main/resources/log4j.xml | 27 ++++++ .../resources/xxl-job-executor.properties | 15 +++ .../pom.xml | 3 - 12 files changed, 376 insertions(+), 29 deletions(-) create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/Application.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/config/FrameLessXxlJobConfig.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/DemoJobHandler.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/HttpJobHandler.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/ShardingJobHandler.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties diff --git a/doc/XXL-JOB官方文档.md b/doc/XXL-JOB官方文档.md index 845046de..06547681 100644 --- a/doc/XXL-JOB官方文档.md +++ b/doc/XXL-JOB官方文档.md @@ -1328,11 +1328,12 @@ Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段 - 2、底层通讯组件迁移至 xxl-rpc; - 3、IP获取逻辑优化,优先遍历网卡来获取可用IP; - 4、任务新增的API服务接口返回任务ID,方便调用方实用; -- 5、[迭代中]任务状态与quartz解耦,降低quartz调度压力,仅NORMAL状态任务绑定quartz; -- 6、[迭代中]新增任务默认运行状态,任务更新时运行状态保持不变; -- 7、[迭代中]原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可,可执行任意命令; -- 8、[迭代中]cron在线生成工具,如 "cronboot/cron.qqe2"; -- 9、[迭代中]docker镜像,并且推送docker镜像到中央仓库,更进一步实现产品开箱即用; +- 5、新增无框架执行器Sample示例项目 "xxl-job-executor-sample-frameless"。不依赖第三方框架,只需main方法即可启动运行执行器; +- 6、[迭代中]任务状态与quartz解耦,降低quartz调度压力,仅NORMAL状态任务绑定quartz; +- 7、[迭代中]新增任务默认运行状态,任务更新时运行状态保持不变; +- 8、[迭代中]原生提供通用命令行任务Handler(Bean任务,"CommandJobHandler");业务方只需要提供命令行即可,可执行任意命令; +- 9、[迭代中]cron在线生成工具,如 "cronboot/cron.qqe2"; +- 10、[迭代中]docker镜像,并且推送docker镜像到中央仓库,更进一步实现产品开箱即用; ### TODO LIST @@ -1359,8 +1360,8 @@ Tips: 历史版本(V1.3.x)目前已经Release至稳定版本, 进入维护阶段 - 21、批量触发支持,添加参数 "org.quartz.scheduler.batchTriggerAcquisitionMaxCount: 50"; - 22、失败重试间隔; - 23、Release发布时,一同发布调度中心安装包,真正实现开箱即用; -- 24、[迭代中]任务权限管理:执行器为粒度分配权限,核心操作校验权限; -- 25、[迭代中]SimpleTrigger 支持; +- 24、任务权限管理:执行器为粒度分配权限,核心操作校验权限; +- 25、SimpleTrigger 支持; ## 七、其他 diff --git a/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java b/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java index bba0ee62..aee94f9f 100644 --- a/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java +++ b/xxl-job-core/src/main/java/com/xxl/job/core/executor/XxlJobExecutor.java @@ -10,7 +10,7 @@ import com.xxl.job.core.thread.ExecutorRegistryThread; import com.xxl.job.core.thread.JobLogFileCleanThread; import com.xxl.job.core.thread.JobThread; import com.xxl.job.core.thread.TriggerCallbackThread; -import com.xxl.rpc.registry.impl.LocalServiceRegistry; +import com.xxl.rpc.registry.ServiceRegistry; import com.xxl.rpc.remoting.invoker.XxlRpcInvokerFactory; import com.xxl.rpc.remoting.invoker.call.CallType; import com.xxl.rpc.remoting.invoker.reference.XxlRpcReferenceBean; @@ -25,9 +25,7 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -38,7 +36,7 @@ public class XxlJobExecutor implements ApplicationContextAware { // ---------------------- param ---------------------- private String adminAddresses; - private static String appName; + private String appName; private String ip; private int port; private String accessToken; @@ -123,7 +121,7 @@ public class XxlJobExecutor implements ApplicationContextAware { // ---------------------- admin-client (rpc invoker) ---------------------- private static List adminBizList; - private static void initAdminBizList(String adminAddresses, String accessToken) throws Exception { + private void initAdminBizList(String adminAddresses, String accessToken) throws Exception { if (adminAddresses!=null && adminAddresses.trim().length()>0) { for (String address: adminAddresses.trim().split(",")) { if (address!=null && address.trim().length()>0) { @@ -155,13 +153,19 @@ public class XxlJobExecutor implements ApplicationContextAware { // ---------------------- executor-server (rpc provider) ---------------------- private XxlRpcInvokerFactory xxlRpcInvokerFactory = null; private XxlRpcProviderFactory xxlRpcProviderFactory = null; + private void initRpcProvider(String ip, int port, String appName, String accessToken) throws Exception { // init invoker factory xxlRpcInvokerFactory = new XxlRpcInvokerFactory(); // init, provider factory + String address = IpUtil.getIpPort(ip, port); + Map serviceRegistryParam = new HashMap(); + serviceRegistryParam.put("appName", appName); + serviceRegistryParam.put("address", address); + xxlRpcProviderFactory = new XxlRpcProviderFactory(); - xxlRpcProviderFactory.initConfig(NetEnum.JETTY, Serializer.SerializeEnum.HESSIAN.getSerializer(), ip, port, accessToken, ExecutorServiceRegistry.class, null); + xxlRpcProviderFactory.initConfig(NetEnum.JETTY, Serializer.SerializeEnum.HESSIAN.getSerializer(), ip, port, accessToken, ExecutorServiceRegistry.class, serviceRegistryParam); // add services xxlRpcProviderFactory.addService(ExecutorBiz.class.getName(), null, new ExecutorBizImpl()); @@ -171,25 +175,32 @@ public class XxlJobExecutor implements ApplicationContextAware { } - public static class ExecutorServiceRegistry extends LocalServiceRegistry { + public static class ExecutorServiceRegistry extends ServiceRegistry { + @Override - public boolean registry(String key, String value) { - + public void start(Map param) { // start registry - if (ExecutorBiz.class.getName().equalsIgnoreCase(key)) { - ExecutorRegistryThread.getInstance().start(appName, value); - } - - return super.registry(key, value); + ExecutorRegistryThread.getInstance().start(param.get("appName"), param.get("address")); } - @Override public void stop() { // stop registry ExecutorRegistryThread.getInstance().toStop(); - - super.stop(); } + + @Override + public boolean registry(String key, String value) { + return false; + } + @Override + public boolean remove(String key, String value) { + return false; + } + @Override + public TreeSet discovery(String key) { + return null; + } + } private void stopRpcProvider() { @@ -217,7 +228,7 @@ public class XxlJobExecutor implements ApplicationContextAware { public static IJobHandler loadJobHandler(String name){ return jobHandlerRepository.get(name); } - private static void initJobHandlerRepository(ApplicationContext applicationContext){ + private void initJobHandlerRepository(ApplicationContext applicationContext){ if (applicationContext == null) { return; } diff --git a/xxl-job-executor-samples/pom.xml b/xxl-job-executor-samples/pom.xml index 37a849b7..97115d33 100644 --- a/xxl-job-executor-samples/pom.xml +++ b/xxl-job-executor-samples/pom.xml @@ -15,6 +15,7 @@ xxl-job-executor-sample-springboot xxl-job-executor-sample-jfinal xxl-job-executor-sample-nutz + xxl-job-executor-sample-frameless \ No newline at end of file diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml new file mode 100644 index 00000000..a56d797e --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.xuxueli + xxl-job-executor-samples + 2.0.0-SNAPSHOT + + xxl-job-executor-sample-frameless + jar + + ${project.artifactId} + Example executor project for spring boot. + http://www.xuxueli.com/ + + + + + + + org.slf4j + slf4j-log4j12 + ${slf4j-api.version} + + + + + com.xuxueli + xxl-job-core + ${project.parent.version} + + + + + \ No newline at end of file diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/Application.java b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/Application.java new file mode 100644 index 00000000..fda416e6 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/Application.java @@ -0,0 +1,33 @@ +package com.xuxueli.executor.sample.frameless; + +import com.xuxueli.executor.sample.frameless.config.FrameLessXxlJobConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * @author xuxueli 2018-10-31 19:05:43 + */ +public class Application { + private static Logger logger = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + + try { + // start + FrameLessXxlJobConfig.getInstance().initXxlJobExecutor(); + + while (true) { + TimeUnit.HOURS.sleep(1); + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + // destory + FrameLessXxlJobConfig.getInstance().destoryXxlJobExecutor(); + } + + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/config/FrameLessXxlJobConfig.java b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/config/FrameLessXxlJobConfig.java new file mode 100644 index 00000000..6cd75727 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/config/FrameLessXxlJobConfig.java @@ -0,0 +1,96 @@ +package com.xuxueli.executor.sample.frameless.config; + +import com.xuxueli.executor.sample.frameless.jobhandler.DemoJobHandler; +import com.xuxueli.executor.sample.frameless.jobhandler.HttpJobHandler; +import com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler; +import com.xxl.job.core.executor.XxlJobExecutor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Properties; + +/** + * @author xuxueli 2018-10-31 19:05:43 + */ +public class FrameLessXxlJobConfig { + private static Logger logger = LoggerFactory.getLogger(FrameLessXxlJobConfig.class); + + + private static FrameLessXxlJobConfig instance = new FrameLessXxlJobConfig(); + public static FrameLessXxlJobConfig getInstance() { + return instance; + } + + + private XxlJobExecutor xxlJobExecutor = null; + + /** + * init + */ + public void initXxlJobExecutor() { + + // registry jobhandler + XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler()); + XxlJobExecutor.registJobHandler("shardingJobHandler", new ShardingJobHandler()); + XxlJobExecutor.registJobHandler("httpJobHandler", new HttpJobHandler()); + + // load executor prop + Properties xxlJobProp = loadProperties("xxl-job-executor.properties"); + + + // init executor + xxlJobExecutor = new XxlJobExecutor(); + xxlJobExecutor.setAdminAddresses(xxlJobProp.getProperty("xxl.job.admin.addresses")); + xxlJobExecutor.setAppName(xxlJobProp.getProperty("xxl.job.executor.appname")); + xxlJobExecutor.setIp(xxlJobProp.getProperty("xxl.job.executor.ip")); + xxlJobExecutor.setPort(Integer.valueOf(xxlJobProp.getProperty("xxl.job.executor.port"))); + xxlJobExecutor.setAccessToken(xxlJobProp.getProperty("xxl.job.accessToken")); + xxlJobExecutor.setLogPath(xxlJobProp.getProperty("xxl.job.executor.logpath")); + xxlJobExecutor.setLogRetentionDays(Integer.valueOf(xxlJobProp.getProperty("xxl.job.executor.logretentiondays"))); + + // start executor + try { + xxlJobExecutor.start(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + /** + * destory + */ + public void destoryXxlJobExecutor() { + if (xxlJobExecutor != null) { + xxlJobExecutor.destroy(); + } + } + + + public static Properties loadProperties(String propertyFileName) { + InputStreamReader in = null; + try { + ClassLoader loder = Thread.currentThread().getContextClassLoader(); + + in = new InputStreamReader(loder.getResourceAsStream(propertyFileName), "UTF-8");; + if (in != null) { + Properties prop = new Properties(); + prop.load(in); + return prop; + } + } catch (IOException e) { + logger.error("load {} error!", propertyFileName); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + logger.error("close {} error!", propertyFileName); + } + } + } + return null; + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/DemoJobHandler.java b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/DemoJobHandler.java new file mode 100644 index 00000000..18071564 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/DemoJobHandler.java @@ -0,0 +1,32 @@ +package com.xuxueli.executor.sample.frameless.jobhandler; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobLogger; + +import java.util.concurrent.TimeUnit; + +/** + * 任务Handler示例(Bean模式) + * + * 开发步骤: + * 1、继承"IJobHandler":“com.xxl.job.core.handler.IJobHandler”; + * 2、注册到执行器工厂:在 "JFinalCoreConfig.initXxlJobExecutor" 中手动注册,注解key值对应的是调度中心新建任务的JobHandler属性的值。 + * 3、执行日志:需要通过 "XxlJobLogger.log" 打印执行日志; + * + * @author xuxueli 2015-12-19 19:43:36 + */ +public class DemoJobHandler extends IJobHandler { + + @Override + public ReturnT execute(String param) throws Exception { + XxlJobLogger.log("XXL-JOB, Hello World."); + + for (int i = 0; i < 5; i++) { + XxlJobLogger.log("beat at:" + i); + TimeUnit.SECONDS.sleep(2); + } + return SUCCESS; + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/HttpJobHandler.java b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/HttpJobHandler.java new file mode 100644 index 00000000..8901d374 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/HttpJobHandler.java @@ -0,0 +1,63 @@ +package com.xuxueli.executor.sample.frameless.jobhandler; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobLogger; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; + +import java.util.concurrent.TimeUnit; + +/** + * 跨平台Http任务 + * + * @author xuxueli 2018-09-16 03:48:34 + */ +public class HttpJobHandler extends IJobHandler { + + @Override + public ReturnT execute(String param) throws Exception { + + // valid + if (param==null || param.trim().length()==0) { + XxlJobLogger.log("URL Empty"); + return FAIL; + } + + // httpclient + HttpClient httpClient = null; + try { + httpClient = new HttpClient(); + httpClient.setFollowRedirects(false); // Configure HttpClient, for example: + httpClient.start(); // Start HttpClient + + // request + Request request = httpClient.newRequest(param); + request.method(HttpMethod.GET); + request.timeout(5000, TimeUnit.MILLISECONDS); + + // invoke + ContentResponse response = request.send(); + if (response.getStatus() != HttpStatus.OK_200) { + XxlJobLogger.log("Http StatusCode({}) Invalid.", response.getStatus()); + return FAIL; + } + + String responseMsg = response.getContentAsString(); + XxlJobLogger.log(responseMsg); + return SUCCESS; + } catch (Exception e) { + XxlJobLogger.log(e); + return FAIL; + } finally { + if (httpClient != null) { + httpClient.stop(); + } + } + + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/ShardingJobHandler.java b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/ShardingJobHandler.java new file mode 100644 index 00000000..b45a24eb --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/java/com/xuxueli/executor/sample/frameless/jobhandler/ShardingJobHandler.java @@ -0,0 +1,34 @@ +package com.xuxueli.executor.sample.frameless.jobhandler; + +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.IJobHandler; +import com.xxl.job.core.log.XxlJobLogger; +import com.xxl.job.core.util.ShardingUtil; + +/** + * 分片广播任务 + * + * @author xuxueli 2017-07-25 20:56:50 + */ +public class ShardingJobHandler extends IJobHandler { + + @Override + public ReturnT execute(String param) throws Exception { + + // 分片参数 + ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo(); + XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal()); + + // 业务逻辑 + for (int i = 0; i < shardingVO.getTotal(); i++) { + if (i == shardingVO.getIndex()) { + XxlJobLogger.log("第 {} 片, 命中分片开始处理", i); + } else { + XxlJobLogger.log("第 {} 片, 忽略", i); + } + } + + return SUCCESS; + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml new file mode 100644 index 00000000..896517e2 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/log4j.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties new file mode 100644 index 00000000..4ae21c77 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-frameless/src/main/resources/xxl-job-executor.properties @@ -0,0 +1,15 @@ +### xxl-job admin address list, such as "http://address" or "http://address01,http://address02" +xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin + +### xxl-job executor address +xxl.job.executor.appname=xxl-job-executor-sample +xxl.job.executor.ip= +xxl.job.executor.port=9995 + +### xxl-job, access token +xxl.job.accessToken= + +### xxl-job log path +xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler +### xxl-job log retention days +xxl.job.executor.logretentiondays=-1 diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml b/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml index a1f7b6f3..87988d44 100644 --- a/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml +++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot/pom.xml @@ -16,9 +16,6 @@ http://www.xuxueli.com/ - UTF-8 - UTF-8 - 1.7