From 7882460ef86acd8fc26477fb25e993752ed6b321 Mon Sep 17 00:00:00 2001 From: Smart Wolf Date: Sat, 21 Dec 2019 13:31:45 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AE=98=E6=96=B9=E7=BD=91?= =?UTF-8?q?=E7=AB=99=E5=B9=B6=E6=8F=90=E4=BA=A4=E5=BC=80=E5=8F=91=E6=8C=87?= =?UTF-8?q?=E5=8D=97=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + misc/package.json | 37 + misc/site/.vuepress/config.js | 49 + misc/site/.vuepress/nav.js | 56 + .../.vuepress/plugin-stat/enhanceAppFile.js | 29 + misc/site/.vuepress/plugin-stat/stat.js | 5 + misc/site/.vuepress/public/logo.png | Bin 0 -> 7509 bytes misc/site/.vuepress/public/logo_big.png | Bin 0 -> 16020 bytes misc/site/.vuepress/public/logo_small.png | Bin 0 -> 11504 bytes .../.vuepress/public/structure_diagram.png | Bin 0 -> 22204 bytes misc/site/README.md | 21 + misc/site/dev_spec.md | 112 ++ misc/site/guide/README.md | 137 ++ misc/site/guide/cache.md | 234 +++ misc/site/guide/configuration.md | 288 +++ misc/site/guide/core.md | 1119 ++++++++++++ misc/site/guide/log.md | 245 +++ misc/site/guide/persistence/README.md | 5 + misc/site/guide/persistence/jdbc.md | 1551 +++++++++++++++++ misc/site/guide/persistence/mongodb.md | 5 + misc/site/guide/persistence/redis.md | 223 +++ misc/site/guide/plugin.md | 324 ++++ misc/site/guide/serv.md | 562 ++++++ misc/site/guide/validation.md | 260 +++ misc/site/guide/webmvc.md | 1502 ++++++++++++++++ misc/site/quickstart.md | 195 +++ 26 files changed, 6963 insertions(+) create mode 100755 misc/package.json create mode 100755 misc/site/.vuepress/config.js create mode 100755 misc/site/.vuepress/nav.js create mode 100755 misc/site/.vuepress/plugin-stat/enhanceAppFile.js create mode 100755 misc/site/.vuepress/plugin-stat/stat.js create mode 100755 misc/site/.vuepress/public/logo.png create mode 100755 misc/site/.vuepress/public/logo_big.png create mode 100755 misc/site/.vuepress/public/logo_small.png create mode 100755 misc/site/.vuepress/public/structure_diagram.png create mode 100755 misc/site/README.md create mode 100755 misc/site/dev_spec.md create mode 100755 misc/site/guide/README.md create mode 100755 misc/site/guide/cache.md create mode 100755 misc/site/guide/configuration.md create mode 100755 misc/site/guide/core.md create mode 100755 misc/site/guide/log.md create mode 100755 misc/site/guide/persistence/README.md create mode 100755 misc/site/guide/persistence/jdbc.md create mode 100755 misc/site/guide/persistence/mongodb.md create mode 100755 misc/site/guide/persistence/redis.md create mode 100755 misc/site/guide/plugin.md create mode 100755 misc/site/guide/serv.md create mode 100755 misc/site/guide/validation.md create mode 100755 misc/site/guide/webmvc.md create mode 100755 misc/site/quickstart.md diff --git a/.gitignore b/.gitignore index 35a9e54b..11bcd0b6 100755 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ bin/ lib/ target/ +node_modules/ +dist/ +package-lock.json + # Package Files # *.jar *.war diff --git a/misc/package.json b/misc/package.json new file mode 100755 index 00000000..4bb945e4 --- /dev/null +++ b/misc/package.json @@ -0,0 +1,37 @@ +{ + "name": "site", + "private": true, + "scripts": { + "build": "vuepress build site", + "dev": "vuepress dev site", + "lint-md": "yarn lint-md:style && yarn lint-md:wording", + "lint-md:style": "remark --quiet --frail .", + "lint-md:wording": "textlint ./site/**/*.md", + "help": "vuepress --help", + "info": "vuepress info site" + }, + "devDependencies": { + "@textlint-rule/textlint-rule-no-unmatched-pair": "^1.0.7", + "@vuepress/plugin-back-to-top": "^1.2.0", + "@vuepress/plugin-google-analytics": "^1.2.0", + "@vuepress/plugin-medium-zoom": "^1.2.0", + "@vuepress/plugin-pwa": "^1.2.0", + "@vuepress/theme-vue": "^1.2.0", + "remark-cli": "^7.0.0", + "remark-lint": "^6.0.5", + "remark-preset-lint-consistent": "^2.0.3", + "remark-preset-lint-recommended": "^3.0.3", + "textlint": "^11.3.1", + "textlint-filter-rule-comments": "^1.2.2", + "textlint-rule-apostrophe": "^1.0.0", + "textlint-rule-common-misspellings": "^1.0.1", + "textlint-rule-diacritics": "^1.0.0", + "textlint-rule-en-capitalization": "^2.0.2", + "textlint-rule-stop-words": "^1.0.17", + "textlint-rule-terminology": "^1.1.30", + "textlint-rule-write-good": "^1.6.2", + "vue-toasted": "^1.1.25", + "vuepress": "^1.2.0", + "vuepress-plugin-flowchart": "^1.4.2" + } +} diff --git a/misc/site/.vuepress/config.js b/misc/site/.vuepress/config.js new file mode 100755 index 00000000..2500a99a --- /dev/null +++ b/misc/site/.vuepress/config.js @@ -0,0 +1,49 @@ +module.exports = { + title: 'YMP', + lang: 'zh-CN', + description: '一个轻量级、模块化、简单而强大的Java应用程序开发框架。', + head: [ + ['link', {rel: 'icon', href: '/logo.png'}], + ['meta', {name: 'theme-color', content: '#3eaf7c'}] + ], + themeConfig: { + title: null, + logo: '/logo.png', + repoLabel: '查看源码', + smoothScroll: true, + editLinks: false, + nav: require('./nav'), + sidebar: { + '/guide/': buildGuideSidebar('指南') + } + }, + plugins: [ + ['@vuepress/back-to-top', true], + ['@vuepress/medium-zoom', true], + require('./plugin-stat/stat') + ] +}; + +function buildGuideSidebar(title) { + return [ + { + title: title, + collapsable: false, + children: [ + '', + 'core', + 'configuration', + 'log', + 'persistence/', + 'persistence/jdbc', + 'persistence/mongodb', + 'persistence/redis', + 'plugin', + 'serv', + 'validation', + 'cache', + 'webmvc' + ] + } + ] +} \ No newline at end of file diff --git a/misc/site/.vuepress/nav.js b/misc/site/.vuepress/nav.js new file mode 100755 index 00000000..3cf3a368 --- /dev/null +++ b/misc/site/.vuepress/nav.js @@ -0,0 +1,56 @@ +module.exports = [ + { + text: '快速上手', + link: '/quickstart' + }, + { + text: '指南', + link: '/guide/' + }, + { + text: '了解更多', + ariaLabel: '了解更多', + items: [ + { + text: '相关文档', + items: [ + { + text: '接口开发技术规范', + link: '/dev_spec.html' + } + ] + }, + { + text: '相关视频', + items: [ + { + text: '基于YMP框架快速搭建JavaWeb工程', + link: 'http://v.youku.com/v_show/id_XMzU2NzQyODI4NA==.html?spm=a2h3j.8428770.3416059.1' + }, + { + text: 'CentOS服务器应用实战——服务环境快速搭建', + link: 'http://v.youku.com/v_show/id_XMzYxODg1NTYxMg==.html?spm=a2h3j.8428770.3416059.1' + } + ] + } + ] + }, + { + text: '查看源码', + ariaLabel: '查看源码', + items: [ + { + items: [ + { + text: 'Gitee(码云)', + link: 'https://gitee.com/suninformation/ymate-platform-v2' + }, + { + text: 'GitHub', + link: 'https://github.com/suninformation/ymate-platform-v2' + } + ] + } + ] + } +]; \ No newline at end of file diff --git a/misc/site/.vuepress/plugin-stat/enhanceAppFile.js b/misc/site/.vuepress/plugin-stat/enhanceAppFile.js new file mode 100755 index 00000000..6d628da4 --- /dev/null +++ b/misc/site/.vuepress/plugin-stat/enhanceAppFile.js @@ -0,0 +1,29 @@ +export default ({router}) => { + if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') { + const script = document.createElement('script'); + script.src = 'https://s4.cnzz.com/z_stat.php?id=1254908110&web_id=1254908110'; + script.language = 'JavaScript'; + document.body.appendChild(script); + + router.afterEach(function (to) { + if (to.path) { + if (window._czc) { + window._czc.push(['_trackPageview', to.fullPath, '/']) + } + // + const _hmt = _hmt || []; + window._hmt = _hmt; + (function () { + document.getElementById('baidu_stat') && document.getElementById('baidu_stat').remove(); + // + const hm = document.createElement("script"); + hm.id = 'baidu_stat'; + hm.src = 'https://hm.baidu.com/hm.js?d732d09a2ccea77b26ad0581cd9bd91c'; + const s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + } + }) + } +} + diff --git a/misc/site/.vuepress/plugin-stat/stat.js b/misc/site/.vuepress/plugin-stat/stat.js new file mode 100755 index 00000000..644c2970 --- /dev/null +++ b/misc/site/.vuepress/plugin-stat/stat.js @@ -0,0 +1,5 @@ +const { path } = require('@vuepress/shared-utils'); + +module.exports = (options = {}, context) => ({ + enhanceAppFiles: path.resolve(__dirname, 'enhanceAppFile.js') +}); \ No newline at end of file diff --git a/misc/site/.vuepress/public/logo.png b/misc/site/.vuepress/public/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..fa4f865c72b6a15cbe67f52f9b9c18efee56b7cd GIT binary patch literal 7509 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 literal 0 HcmV?d00001 diff --git a/misc/site/.vuepress/public/logo_big.png b/misc/site/.vuepress/public/logo_big.png new file mode 100755 index 0000000000000000000000000000000000000000..bfa10f3de574f44154a9d21ed43f12c368bda590 GIT binary patch literal 16020 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 literal 0 HcmV?d00001 diff --git a/misc/site/.vuepress/public/structure_diagram.png b/misc/site/.vuepress/public/structure_diagram.png new file mode 100755 index 0000000000000000000000000000000000000000..85d1b1eb131fa5015b472eeb42404d5a8e7c573c GIT binary patch literal 22204 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 Dis1gq literal 0 HcmV?d00001 diff --git a/misc/site/README.md b/misc/site/README.md new file mode 100755 index 00000000..ce0c3b30 --- /dev/null +++ b/misc/site/README.md @@ -0,0 +1,21 @@ +--- +home: true +heroImage: /logo_big.png +heroText: null +tagline: 轻量级、组件化、简单高效的Java应用开发框架 +actionText: 快速上手 +actionLink: /quickstart +features: +- title: 轻量级 + details: 采用微内核实现AutoScan、AOP、IoC、Event等特性,涵盖SSH框架中绝大部分核心功能! +- title: 组件化 + details: 采用模块方式打包,按需装配,灵活扩展,独特的服务开发体验,完善的插件机制,助力于更细颗粒度的业务拆分! +- title: 简单高效 + details: 统一日志系统和配置体系结构,轻量的持久层封装,灵活的缓存服务,配置简单的MVC和参数验证,让您更专注于业务! +footer: Apache License Version 2.0 | Copyright © 2015-2019 yMate.Net. All Rights Reserved. +--- + + +::: tip 提示 +当前版本 = `2.0.8` +::: diff --git a/misc/site/dev_spec.md b/misc/site/dev_spec.md new file mode 100755 index 00000000..5b51c649 --- /dev/null +++ b/misc/site/dev_spec.md @@ -0,0 +1,112 @@ +--- +sidebar: auto +sidebarDepth: 2 +--- + +# 接口开发技术规范 + +## 基础数据类型约定 + +|类型名称|Java类型|字段类型|说明| +|---|---|---|---| +|布尔型|Integer|smallint(1)|用`1`表示`true`,用`0`表示`false`| +|日期时间型|Long|bigint(13)|用毫秒数值表示,长度为`13`位| +|金额 / 货币型|Long|bigint|采用整数形式存储(即去除小数,以`分/厘`为单位)| + +## 数据库及表字段命名原则 + +表名称格式:[`前缀_`]<`名称段1`[`_名称段n`]> + +字段名称格式:[`is_`]<`名称段1`[`_名称段n`]> + +说明: + +- 数据库表和字段名称由`A-Z`,`a-z`,`0-9`和`_`下划线组成,尽量避免出现数字,多个单词之间用`_`下划线分隔,单词不允许使用复数形式; +- 数据库表和字段名称除数据库特殊情况外,应尽量采用小写英文单词或英文短语或相应缩写,禁止使用汉语拼音和汉字; +- 数据表主键名称尽量使用`id`,避免使用复合主键,建议使用索引替代; +- 布尔型字段名称以`is_`作为前缀,如:`is_deleted`; +- 数据库中字段应预留`create_time`和`last_modify_time`作为版本字段; + +示例: + +- 数据表名称: + + ymate_user + ymate_user_attribute + +- 字段名称: + + is_deleted + + create_time + last_modify_time + +## 接口通用签名规则 + +- 将所有发送或者接收到的数据放入集合中,将集合中的参数名称按`ASCII`码从小到大(字典序)排序; +- 参数值为空不参与签名; +- 参数名称区分大小写且参数内容不要进行`URLEncoder`编码处理; +- 签名参数(如:`signature`)本身不参与签名; +- 将参与签名的参数以URL键值对的格式进行拼接,示例如下: + + String _signStr = "client_id=CLIENT_ID&create_time=1487178385184&event=subscribe&qrscene=QRSCENE&union_id=o6_bmasdasdsad6_2sgVt7hMZOPfL" + +- 将拼接好的`_signStr`字符串与`client_secret`密钥进行拼接并生成签名,示例如下: + + // 拼接密钥 + _signStr = _signStr + "&client_secret=6bf18fa2f9a136273fb90e58dff4a964"; + // 执行MD5签名并将字符转换为大写 + _signStr = MD5(_signStr).toUpperCase(); + +- 如果有必要,可以在请求参数集合中增加随机参数(如:`nonce_str`),通过随机数函数生成并转换为字符串,从而保证签名的不可预测性; +- 客户端与服务端均采用相同规则进行参数签名后进行结果比对,两端签名结果一致则验签通过; + +## 接口响应报文基础结构 + +基础报文示例一: + + { + "ret": -1, + "msg": "参数验证无效", + "data": { + "username": "用户名称不能为空", + "passwd": "登录密码不能为空" + } + } + +基础报文示例二: + + { "ret": -50, "msg": "系统忙,请稍后重试" } + +参数说明: + +|参数|类型|是否必须|说明| +|---|---|---|---| +|ret|int|是|返回码| +|msg|string|否|消息内容,当`ret!=0`则此项为必须项| +|data|object|否|业务数据内容,根据业务逻辑决定其具体数据类型及是否为必须项| + +## 全局返回码说明 + +> `ret = 0`:正确返回; +> +> `ret > 0`:具体业务相关的接口调用错误; +> +> `-50 < ret < 0`:接口调用不能通过接口服务校验; +> +> `ret <= -50`:系统内部错误; + +全局返回码: + +|返回码|说明| +|---|---| +|0|请求成功| +|-1|参数验证无效| +|-2|访问的资源未找到或不存在| +|-3|请求的方法不支持或不正确| +|-4|请求的资源未授权或无权限| +|-5|用户会话无效或超时| +|-6|请求的操作被禁止| +|-7|用户会话已授权(登录)| +|-20|数据版本不匹配| +|-50|系统内部错误| \ No newline at end of file diff --git a/misc/site/guide/README.md b/misc/site/guide/README.md new file mode 100755 index 00000000..f22a1696 --- /dev/null +++ b/misc/site/guide/README.md @@ -0,0 +1,137 @@ +--- +sidebarDepth: 2 +--- + +# 概述 + +LOGO + +> A lightweight modular simple and powerful Java application development framework. + +> 一个非常简单、易用的一套轻量级Java应用开发框架,设计原则主要侧重于简化工作任务、规范开发流程、提高开发效率,让开发工作像搭积木一样轻松是我们一直不懈努力的目标! + +## 技术特点 + +> - 采用组件化、模块方式打包,可按需装配,灵活可扩展; +> - 采用微内核实现`AutoScan`、`AOP`、`IoC`、`Event`等,涵盖`SSH&M`框架中绝大部分核心功能; +> - 统一配置体系结构,感受不一样的文件资源配置及管理模式; +> - 整合多种日志系统(`Log4j`、`JCL`、`Slf4j`等)、日志文件可分离存储; +> - 轻量级持久化层封装,针对`RDBMS`(`MySQL`、`SQLServer`、`Oracle`、`PostgreSQL`等)和`NoSQL`(`MongoDB`、`Redis`等)提供支持; +> - 完善的插件机制,助力于更细颗粒度的业务拆分; +> - 独特的独立服务开发体验; +> - 功能强大的验证框架,完全基于`Java`注解,易于使用和扩展; +> - 灵活的缓存服务,支持`EhCache`、`Redi`s和多级缓存(`MultiLevel`)技术; +> - 配置简单的`MVC`架构,强大且易于维护和扩展,支持`RESTful`风格,支持`JSP`、`HTML`、`Binary`、`Freemarker`、`Velocity`、`Beetl`等多种视图技术; + +## 模块及功能 + +`YMP`框架主要是由核心(`Core`)和若干模块(`Modules`)组成,整体结构简约、清晰,如图所示: + +Structure Diagram + +### 核心(Core) + +主要负责框架的初始化和模块的加载及其生命周期管理,功能包括: + +> - `Beans`:类对象管理器(微型的`Spring`容器),提供包类的自动扫描(`AutoScan`)以及类生命周期管理、依赖注入(`IoC`)和方法拦截(`AOP`)等特性; +> - `Event`:事件服务,通过事件注册和广播的方式触发和监听事件动作,并支持同步和异步两种模式执行事件队列; +> - `Module`:模块(是`YMP`框架所有功能特性封装的基础形式),负责模块的生命周期管理,模块将在框架初始化时自动加载并初始化,在框架销毁时自动销毁; +> - `I18N`:国际化资源管理器,提供统一的资源文件加载、销毁和内容读取,支持自定义资源加载和语言变化的事件监听; +> - `Lang`:提供了一组自定义的数据结构,它们在部分模块中起到了重要的作用,包括: +> + `BlurObject`:用于解决常用数据类型间转换的模糊对象; +> + `PairObject`:用于将两个独立的对象捆绑在一起的结对对象; +> + `TreeObject`:使用级联方式存储各种数据类型,不限层级深度的树型对象; +> - `Util`:提供框架中需要的各种工具类; + +### 配置体系 (Configuration) + +通过简单的目录结构实现在项目开发以及维护过程中,对配置文件等各种资源的统一管理,为模块化开发和部署提供灵活的、简单有效的解决方案: + +> - 从开发角度规范了模块化开发流程、统一资源文件的生命周期管理; +> - 从可维护角度将全部资源集成在整个体系中,具备有效的资源重用和灵活的系统集成构建、部署和数据备份与迁移等优势; +> - 简单的配置文件检索、加载及管理模式; +> - 模块间资源共享,模块(`modules`)可以共用所属项目(`projects`)的配置、类和`JAR`包等资源文件; +> - 默认支持`XML`和`Properties`配置文件解析,可以通过`IConfigurationProvider`接口自定义文件格式,支持缓存,避免重复加载; +> - 配置对象支持`@Configuration`注解方式声明,无需编码即可自动加载并填充配置内容到类对象; +> - 修改配置文件无需重启服务,支持自动重新加载; +> - 集成模块的构建(编译)与分发、服务的启动与停止,以及清晰的资源文件分类结构可快速定位; + +### 日志 (Log) + +基于开源日志框架`Log4j 2`实现,提供对日志记录器对象的统一管理,可以在任意位置调用任意日志记录器输出日志,实现系统与业务日志的分离,并针对`apache-commons-logging`日志框架和`Slf4j`日志系统提供支持; + +### 持久化 (Persistence) + +#### JDBC + +针对关系型数据库(`RDBMS`)数据存取的一套简单解决方案,主要关注数据存取的效率、易用性和透明,其具备以下功能特性: + +> - 基于`JDBC`框架`API`进行轻量封装,结构简单、便于开发、调试和维护; +> - 优化批量数据更新、标准化结果集、预编译`SQL`语句处理; +> - 支持单实体`ORM`操作,无需编写`SQL`语句; +> - 提供脚手架工具,快速生成数据实体类,支持链式调用; +> - 支持通过存储器注解自定义`SQL`语句或从配置文件中加载`SQL`并自动执行; +> - 支持结果集与值对象的自动装配,支持自定义装配规则; +> - 支持多数据源,默认支持`C3P0`、`DBCP`、`JND`I连接池配置,支持数据源扩展; +> - 支持多种数据库(如:`Oracle`、`MySQL`、`SQLServer`、`SQLite`、`H2`、`PostgreSQL`等); +> - 支持面向对象的数据库查询封装,有助于减少或降低程序编译期错误; +> - 支持数据库事务嵌套; +> - 支持数据库视图和存储过程; + +#### MongoDB + +针对`MongoDB`的数据存取操作的特点,以`JDBC`模块的设计思想进行简单封装,采用会话机制,支持多数据源配置和实体操作、基于对象查询、`MapReduce`、`GridFS`、聚合及函数表达式集成等; + +#### Redis + +基于`Jedis`驱动封装,以`JDBC`模块的设计思想进行简单封装,采用会话机制,简化订阅(`subscribe`)和发布(`publish`)处理,支持多数据源及连接池配置,支持`jedis`、`shard`、`sentinel`和`cluster`等数据源连接方式; + +### 插件 (Plugin) + +采用独立的`ClassLoader`类加载器来管理私有`JAR`包、类、资源文件等,设计目标是在接口开发模式下,将需求进行更细颗粒度拆分,从而达到一个理想化可重用代码的封装形态; + +每个插件都是封闭的世界,插件与外界之间沟通的唯一方法是通过业务接口调用,管理这些插件的容器被称之为插件工厂(`IPluginFactory`),负责插件的分析、加载和初始化,以及插件的生命周期管理,插件模块支持创建多个插件工厂实例,工厂对象之间完全独立,无任何依赖关系; + +### 服务 (Serv) + +基于`NIO`实现的通讯服务框架,提供`TCP`、`UDP`协议的客户端(`Client`)与服务端(`Server`)封装,灵活的消息监听与消息内容编/解码,简约的配置使二次开发更加便捷; + +同时默认提供服务端会话管理和客户端断线重连、链路维护(心跳)等服务支持,您只需了解业务即可轻松完成开发工作; + +### 验证 (Validation) + +服务端参数有效性验证工具,采用注解声明方式配置验证规则,更简单、更直观、更友好,支持方法参数和类成员属性验证,支持验证结果国际化`I18N`资源绑定,支持自定义验证器,支持多种验证模式; + +### 缓存 (Cache) + +以`EhCache`作为默认`JVM`进程内缓存服务,通过整合外部`Redis`服务实现多级缓存(`MultiLevel`)的轻量级缓存框架,并与`YMP`框架深度集成(支持针对类方法的缓存,可以根据方法参数值进行缓存),灵活的配置、易于使用和扩展; + +### WebMVC + +`WebMVC`模块在`YMP`框架中是除了`JDBC`模块以外的另一个非常重要的模块,集成了`YMP`框架的诸多特性,在功能结构的设计和使用方法上依然保持一贯的简单风格,同时也继承了主流MVC框架的基因,对于了解和熟悉`SSH&M`等框架技术的开发人员来说,上手极其容易,毫无学习成本; + +其主要功能特性如下: + +> - 标准`MVC`实现,结构清晰,完全基于注解方式配置简单; +> - 支持约定(`Conversion`)模式,无需编写控制器代码,直接匹配并执行视图; +> - 支持多种视图技术(`JSP`、`Freemarker`、`Velocity`、`Text`、`HTML`、`JSON`、`Binary`、`Forward`、`Redirect`、`HttpStatus`、`Beetl`等); +> - 支持`RESTful`模式及`URL`风格; +> - 支持请求参数与控制器方法参数的自动绑定; +> - 支持参数有效性验证; +> - 支持控制器方法的拦截; +> - 支持注解配置控制器请求路由映射; +> - 支持自动扫描控制器类并注册; +> - 支持事件和异常的自定义处理; +> - 支持`I18N`资源国际化; +> - 支持控制器方法和视图缓存; +> - 支持控制器参数转义; +> - 支持插件扩展; + +::: tip One More Thing + +`YMP`不仅提供便捷的`Web`及其它`Java`项目的快速开发体验,也将不断提供更多丰富的项目实践经验。 + +感兴趣的小伙伴儿们可以加入 官方`QQ`群`480374360`,一起交流学习,帮助`YMP`成长! + +了解更多有关`YMP`框架的内容,请访问官网:[https://ymate.net](https://ymate.net) +::: diff --git a/misc/site/guide/cache.md b/misc/site/guide/cache.md new file mode 100755 index 00000000..6d285541 --- /dev/null +++ b/misc/site/guide/cache.md @@ -0,0 +1,234 @@ +--- +sidebarDepth: 2 +--- + +# 缓存(Cache) + +缓存模块是以EhCache作为默认JVM进程内缓存服务,通过整合外部Redis服务实现多级缓存(MultiLevel)的轻量级缓存框架,并与YMP框架深度集成(支持针对类方法的缓存,可以根据方法参数值进行缓存),灵活的配置、易于使用和扩展; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-cache + + + +> **注**: +> - 在项目的pom.xml中添加上述配置,该模块已经默认引入核心包依赖,无需重复配置。 +> - 若需要启用redis作为缓存服务,请添加以下依赖配置: +> +> +> net.ymate.platform +> ymate-platform-persistence-redis +> +> + +## 基础接口概念 + +开发者可以根据以下接口完成对缓存模块的自定义扩展实现; + +- 缓存服务提供者(ICacheProvider)接口: + + + DefaultCacheProvider - 基于EhCache缓存服务的默认缓存服务提供者接口实现类; + + RedisCacheProvider - 基于Redis数据库的缓存服务提供者接口实现类; + + MultievelCacheProvider - 融合EhCache和Redis两者的缓存服务提供者接口实现类,通过MultilevelKey决定缓存对象的获取方式; + +- 缓存Key生成器(IKeyGenerator)接口: + + + DefaultKeyGenerator - 根据提供的类方法和参数对象生成缓存Key,默认是将方法和参数对象进行序列化后取其MD5值; + +- 序列化服务(ISerializer)接口: + + + DefaultSerializer - 默认序列化服务采用JDK自带的对象序列化技术实现; + +- 缓存事件监听(ICacheEventListener)接口:用于监听被缓存对象发生变化时的事件处理,需开发者实现接口; + +- 缓存作用域处理器(ICacheScopeProcessor)接口:用于处理@Cacheable注解的Scope参数设置为非DEFAULT作用域的缓存对象,需开发者实现接口; + +## 模块配置 + +### 初始化参数配置 + + #------------------------------------- + # 缓存模块初始化参数 + #------------------------------------- + + # 缓存提供者,可选参数,默认值为default,目前支持[default|redis|multilevel]或自定义类名称 + ymp.configs.cache.provider_class= + + # 缓存对象事件监听器,可选参数,默认值为net.ymate.platform.cache.impl.DefaultCacheEventListener + ymp.configs.cache.event_listener_class= + + # 缓存作用域处理器,可选参数,默认值为空 + ymp.configs.cache.scope_processor_class= + + # 缓存Key生成器,可选参数,默认采用框架默认net.ymate.platform.cache.impl.DefaultKeyGenerator + ymp.configs.cache.key_generator_class= + + # 对象序列化接口实现,可选参数,默认值为ISerializer.SerializerManager.getDefaultSerializer() + ymp.configs.cache.serializer_class= + + # 默认缓存名称,可选参数,默认值为default,对应于Ehcache配置文件中设置name="__DEFAULT__" + ymp.configs.cache.default_cache_name= + + # 缓存数据超时时间,可选参数,数值必须大于等于0,为0表示默认缓存300秒 + ymp.configs.cache.default_cache_timeout= + + # 是否采用SET进行缓存数据存储,默认值为false + ymp.params.cache.storage_with_set= + + # 禁用Redis订阅缓存元素过期事件,可选参数,默认值为false + ymp.params.cache.disabled_subscribe_expired= + + # Multilevel模式下是否自动同步Master和Slave级缓存,可选扩展参数, 默认值为false + ymp.params.cache.multilevel_slave_autosync= + +### EhCache配置示例 + +请将以下内容保存在ehcache.xml文件中,并放置在classpath根路径下; + + + + + + + + + + + + + +## 模块事件 + +当`event_listener_class`采用默认配置时,可以通过CacheEvent捕获缓存事件,事件枚举对象包括以下事件类型: + +|事务类型|说明| +|---|---| +|ELEMENT_PUT|添加元素到缓存| +|ELEMENT_UPDATED|缓存元素更新| +|ELEMENT_EXPIRED|缓存元素过期| +|ELEMENT_EVICTED|| +|ELEMENT_REMOVED|缓存元素删除| +|ELEMENT_REMOVED_ALL|缓存清空| + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置缓存模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg(CacheModuleConfigurable.create() + .defaultCacheName("default") + .defaultCacheTimeout(7200) + .serializerClass("default") + .providerClass(ICache.DEFAULT)).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + // 扩展参数配置 + .param(ICacheModuleCfg.PARAMS_CACHE_STORAGE_WITH_SET, "false") + .param(ICacheModuleCfg.PARAMS_CACHE_DISABLED_SUBSCRIBE_EXPIRED, "false") + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Caches.class); + // 执行框架初始化 + owner.init(); + +## 模块使用 + +### 示例一:直接通过缓存模块操作缓存数据 + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // 操作默认缓存 + Caches.get().put("key1", "value1"); + System.out.println(Caches.get().get("key1")); + // 操作指定名称的缓存 + Caches.get().put("default", "key2", "value2"); + System.out.println(Caches.get().get("default", "key2")); + } finally { + YMP.get().destroy(); + } + } + +**注**:当指定缓存名称时,请确认与名称对应的配置是否已存在; + +执行结果: + + value1 + value2 + +### 示例二:基于注解完成类方法的缓存 + +这里用到了@Cacheable注解,作用是标识类中方法的执行结果是否进行缓存,需要注意的是: + +> 首先@Cacheable注解必须在已注册到YMP类对象管理器的类上声明,表示该类支持缓存; +> +> 其次,在需要缓存执行结果的方法上添加@Cacheable注解; + +@Cacheable注解参数说明: + +> cacheName:缓存名称, 默认值为default; +> +> key:缓存Key, 若未设置则使用keyGenerator自动生成; +> +> generator:Key生成器接口实现类,默认为DefaultKeyGenerator.class; +> +> scope:缓存作用域,可选值为APPLICATION、SESSION和DEFAULT,默认为DEFAULT,非DEFAULT设置需要缓存作用域处理器(ICacheScopeProcessor)接口配合; +> +> timeout:缓存数据超时时间, 可选参数,数值必须大于等于0,为0表示默认缓存300秒; + +示例代码: + + @Bean + @Cacheable + public class CacheDemo { + + @Cacheable + public String sayHi(String name) { + System.out.println("No Cached"); + return "Hi, " + name; + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + CacheDemo _demo = YMP.get().getBean(CacheDemo.class); + System.out.println(_demo.sayHi("YMP")); + System.out.println(_demo.sayHi("YMP")); + // + System.out.println("--------"); + // + System.out.println(_demo.sayHi("YMPer")); + System.out.println(_demo.sayHi("YMP")); + System.out.println(_demo.sayHi("YMPer")); + } finally { + YMP.get().destroy(); + } + } + } + +执行结果: + + No Cached + Hi, YMP + Hi, YMP + -------- + No Cached + Hi, YMPer + Hi, YMP + Hi, YMPer + +以上结果输出可以看出,sayHi方法相同参数首次被调用时将输出“No Cached”字符串,说明它没有使用缓存,再次调用时直接从缓存中返回值; diff --git a/misc/site/guide/configuration.md b/misc/site/guide/configuration.md new file mode 100755 index 00000000..66c768e9 --- /dev/null +++ b/misc/site/guide/configuration.md @@ -0,0 +1,288 @@ +--- +sidebarDepth: 2 +--- + +# 配置体系 (Configuration) + +配置体系模块,是通过简单的目录结构实现在项目开发以及维护过程中,对配置等各种文件资源的统一管理,为模块化开发和部署提供灵活的、简单有效的解决方案; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-configuration + + + +> **注**:在项目的pom.xml中添加上述配置,该模块已经默认引入核心包依赖,无需重复配置。 + +## 特点 + +- 从开发角度规范了模块化开发流程、统一资源文件的生命周期管理; +- 从可维护角度将全部资源集成在整个体系中,具备有效的资源重用和灵活的系统集成构建、部署和数据备份与迁移等优势; +- 简单的配置文件检索、加载及管理模式; +- 模块间资源共享,模块(modules)可以共用所属项目(projects)的配置、类和jar包等资源文件; +- 默认支持XML和Properties配置文件解析,可以通过IConfigurationProvider接口自定义文件格式,支持缓存,避免重复加载; +- 配置对象支持`@Configuration`注解方式声明,无需编码即可自动加载并填充配置内容到类对象; +- 修改配置文件无需重启服务,支持自动重新加载; +- 集成模块的构建(编译)与分发、服务的启动与停止,以及清晰的资源文件分类结构可快速定位; + +## 配置体系目录结构 + +按优先级由低到高的顺序依次是:全局(configHome) -> 项目(projects) -> 模块(modules): + + + CONFIG_HOME\ + |--bin\ + |--cfgs\ + |--classes\ + |--dist\ + |--lib\ + |--logs\ + |--plugins\ + |--projects\ + | |-- + | | |--cfgs\ + | | |--classes\ + | | |--lib\ + | | |--logs\ + | | |--modules\ + | | | |-- + | | | | |--cfgs\ + | | | | |--classes\ + | | | | |--lib\ + | | | | |--logs\ + | | | | |--plugins\ + | | | | |--<......> + | | | |--<......> + | | |--plugins\ + | |--<......> + |--temp\ + |--...... + +## 模块配置 + +配置体系模块初始化参数, 将下列配置项按需添加到ymp-conf.properties文件中, 否则模块将使用默认配置进行初始化: + + + #------------------------------------- + # 配置体系模块初始化参数 + #------------------------------------- + + # 配置体系根路径,必须绝对路径,前缀支持${root}、${user.home}和${user.dir}变量,默认为${root} + ymp.configs.configuration.config_home= + + # 项目名称,做为根路径下级子目录,对现实项目起分类作用,默认为空 + ymp.configs.configuration.project_name= + + # 模块名称,此模块一般指现实项目中分拆的若干子项目的名称,默认为空 + ymp.configs.configuration.module_name= + + # 配置文件检查时间间隔(毫秒),默认值为0表示不开启 + ymp.configs.configuration.config_check_time_interval= + + # 指定配置体系下的默认配置文件分析器,默认为net.ymate.platform.configuration.impl.DefaultConfigurationProvider + ymp.configs.configuration.provider_class= + +> **注**:配置体系根路径`config_home`配置参数,可以通过`JVM`启动参数方式进行配置,如:`java -jar -Dymp.config_home=...`,这种方式将优先于配置文件。 + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置配置体系模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + ConfigModuleConfigurable.create() + .configHome("${root}") + .projectName("demo") + .moduleName("core") + .providerClass(DefaultConfigurationProvider.class) + .configCheckTimeInterval(30000L)).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Cfgs.class); + // 执行框架初始化 + owner.init(); + // 销毁 + owner.destroy(); + +## 示例一:解析XML配置 + +- 基于XML文件的基础配置格式如下, 为了配合测试代码, 请将该文件命名为configuration.xml并放置在`config_home`路径下的cfgs目录里: + + + + + + + + + + + + + + + + iphone + ipad + imac + itouch + + + + + + red + 120g + small + 2015 + + + + + +- 新建配置类DemoConfig, 通过`@Configuration`注解指定配置文件相对路径 + + + @Configuration(value = "cfgs/configuration.xml", reload = true) + public class DemoConfig extends DefaultConfiguration { + } + + +- 测试代码, 完成模块初始化并加载配置文件内容: + + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + DemoConfig _cfg = new DemoConfig(); + if (Cfgs.get().fillCfg(_cfg)) { + System.out.println(_cfg.getString("company_name")); + System.out.println(_cfg.getMap("product_spec")); + System.out.println(_cfg.getList("products")); + } + } finally { + YMP.get().destroy(); + } + } + +- 执行结果: + + + Apple Inc. + {abc=xzy, color=red, size=small, weight=120g, age=2015} + [itouch, imac, ipad, iphone] + +## 示例二:解析Properties配置 + +- 基于Properties文件的基础配置格式如下, 同样请将该文件命名为configuration.properties并放置在`config_home`路径下的cfgs目录里: + + + #-------------------------------------------------------------------------- + # 配置文件内容格式: properties..=[propertyValue] + # + # 注意: attributes将作为关键字使用, 用于表示分类, 属性, 集合和MAP的子属性集合 + #-------------------------------------------------------------------------- + + # 举例1: 默认分类下表示公司名称, 默认分类名称为default + properties.default.company_name=Apple Inc. + + #-------------------------------------------------------------------------- + # 数组和集合数据类型的表示方法: 多个值之间用'|'分隔, 如: Value1|Value2|...|ValueN + #-------------------------------------------------------------------------- + properties.default.products=iphone|ipad|imac|itouch + + #-------------------------------------------------------------------------- + # MAP数据类型的表示方法: + # 如:产品规格(product_spec)的K分别是color|weight|size|age, 对应的V分别是热red|120g|small|2015 + #-------------------------------------------------------------------------- + properties.default.product_spec.color=red + properties.default.product_spec.weight=120g + properties.default.product_spec.size=small + properties.default.product_spec.age=2015 + + # 每个MAP都有属于其自身的属性列表(深度仅为一级), 用attributes表示, abc代表属性key, xyz代表属性值 + # 注: MAP数据类型的attributes和MAP本身的表示方法达到的效果是一样的 + properties.default.product_spec.attributes.abc=xyz + + +- 修改配置类DemoConfig如下, 通过`@ConfigurationProvider`注解指定配置文件内容解析器: + + + @Configuration("cfgs/configuration.properties") + @ConfigurationProvider(PropertyConfigurationProvider.class) + public class DemoConfig extends DefaultConfiguration { + } + + +- 重新执行示例代码, 执行结果与示例一结果相同: + + + Apple Inc. + {abc=xzy, color=red, size=small, weight=120g, age=2015} + [itouch, imac, ipad, iphone] + +## 示例三:无需创建配置对象, 直接加载配置文件 + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + IConfiguration _cfg = Cfgs.get().loadCfg("cfgs/configuration.properties"); + if (_cfg != null) { + System.out.println(_cfg.getString("company_name")); + System.out.println(_cfg.getMap("product_spec")); + System.out.println(_cfg.getList("products")); + } + } finally { + YMP.get().destroy(); + } + } + +## 示例四:通过`@Configurable`注解并配合`IConfigurable`接口实现配置文件自动装配 + + public interface IDemoService { + + String getCompanyName(); + } + + @Bean + @Configurable(type = DemoConfig.class) + public class DemoService extends AbstractConfigurable implements IDemoService { + + @Override + public String getCompanyName() { + return getConfig().getString("company_name"); + } + } + +## 配置体系模块更多操作 + +### 获取路径信息 + +下列方法的返回结果会根据配置体系模块配置的不同而不同: + + // 返回配置体系根路径 + Cfgs.get().getConfigHome(); + + // 返回项目根路径 + Cfgs.get().getProjectHome(); + + // 返回项目模块根路径 + Cfgs.get().getModuleHome(); + + // 返回user.dir所在路径 + Cfgs.get().getUserDir(); + + // 返回user.home所在路径 + Cfgs.get().getUserHome(); + +### 搜索目标文件 + + // 在配置体系中搜索cfgs/configuration.xml文件并返回其File对象 + Cfgs.get().searchFile("cfgs/configuration.xml"); + + // 在配置体系中搜索cfgs/configuration.properties文件并返回其绝对路径 + Cfgs.get().searchPath("cfgs/configuration.properties"); diff --git a/misc/site/guide/core.md b/misc/site/guide/core.md new file mode 100755 index 00000000..a6f720dd --- /dev/null +++ b/misc/site/guide/core.md @@ -0,0 +1,1119 @@ +--- +sidebarDepth: 2 +--- + +# 核心 (Core) + +YMP框架主要是由核心(Core)和若干模块(Modules)组成,核心主要负责框架的初始化和模块的生命周期管理。 + +## 核心功能 + +- Beans:类对象管理器(微型的Spring容器),提供包类的自动扫描(AutoScan)以及Bean生命周期管理、依赖注入(IoC)和方法拦截(AOP)等特性。 + +- Event:事件服务,通过事件注册和广播的方式触发和监听事件动作,并支持同步和异步两种模式执行事件队列。 + +- Module:模块,是YMP框架所有功能特性封装的基础形式,负责模块的生命周期管理,模块将在框架初始化时自动加载并初始化,在框架销毁时自动销毁。 + +- I18N:国际化资源管理器,提供统一的资源文件加载、销毁和内容读取,支持自定义资源加载和语言变化的事件监听。 + +- Lang:提供了一组自定义的数据结构,它们在部分模块中起到了重要的作用,包括: + + BlurObject:用于解决常用数据类型间转换的模糊对象。 + + PairObject:用于将两个独立的对象捆绑在一起的结对对象。 + + TreeObject:使用级联方式存储各种数据类型,不限层级深度的树型对象。 + +- Util:提供框架中需要的各种工具类。 + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-core + + + +> **注**:若想单独使用YMP核心包时需要在pom.xml中添加上述配置,其它模块已经默认引入核心包依赖,无需重复配置。 + +## 框架初始化 + +### 方式一:基于配置文件初始化 + +YMP框架的初始化默认是从加载`ymp-conf.properties`文件开始的,该文件必须被放置在`classpath`的根路径下; + +- 根据程序运行环境的不同,YMP框架初始化时将根据当前操作系统优先级加载配置: + + + 优先加载`ymp-conf_DEV.properties`(若加载成功则强制设置`ymp.dev_mode=true`) + + Unix/Linux环境下,优先加载`ymp-conf_UNIX.properties`; + + Windows环境下,优先加载`ymp-conf_WIN.properties`; + + 若以上配置文件未找到,则加载默认配置`ymp-conf.properties`; + +- 同时,也可以通过JVM启动参数配置系统环境,框架将优先根据当前操作系统及运行环境加载匹配的配置文件: + + + `-Dymp.run_env=test`:测试环境,将优先加载`ymp-conf_TEST.properties` + + `-Dymp.run_env=dev`:开发环境,将优先加载`ymp-conf_DEV.properties` + + `-Dymp.run_env=product`:生产环境,将优先加载`ymp-conf.properties` + +- 框架初始化基本配置参数: + + #------------------------------------- + # 框架基本配置参数 + #------------------------------------- + + # 是否为开发模式,默认为false + ymp.dev_mode= + + # 框架自动扫描的包路径集合,多个包名之间用'|'分隔,默认已包含net.ymate.platform包,其子包也将被扫描 + ymp.autoscan_packages= + + # 包排除列表,多个包名之间用'|'分隔,被包含在包路径下的类文件在扫描过程中将被忽略 + ymp.excluded_packages= + + # 包文件排除列表,多个文件名称之间用'|'分隔,被包含的JAR或ZIP文件在扫描过程中将被忽略 + ymp.excluded_files= + + # 模块排除列表,多个模块名称或类名之间用'|'分隔,被包含的模块在加载过程中将被忽略 + ymp.excluded_modules= + + # 框架对象加载器, 可选参数, 默认为net.ymate.platform.core.beans.impl.DefaultBeanLoader + ymp.bean_loader_class= + + # 框架代理工厂, 可选参数, 默认为net.ymate.platform.core.beans.proxy.impl.DefaultProxyFactory + ymp.proxy_factory_class= + + # 国际化资源默认语言设置,可选参数,默认采用系统环境语言 + ymp.i18n_default_locale=zh_CN + + # 国际化资源管理器事件监听处理器,可选参数,默认为空 + ymp.i18n_event_handler_class= + + # 默认密码处理器,可选参数,用于对已加密参数值进行解密,默认为net.ymate.platform.core.support.impl.DefaultPasswordProcessor + ymp.default_password_class= + + # 框架全局自定义参数,xxx表示自定义参数名称,vvv表示参数值 + ymp.params.xxx=vvv + + # 本文测试使用的自定义参数 + ymp.params.helloworld=Hello, YMP! + + +- 测试代码,完成框架的启动和销毁: + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // 输出自定义参数值:Hello, YMP! + System.out.println(YMP.get().getConfig().getParam("helloworld")); + } finally { + YMP.get().destroy(); + } + } + +### 方式二:通过代码初始化 + +- 示例代码,采用自定代理和对象加载器完成框架初始化操作: + + public static void main(String[] args) throws Exception { + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create() + .proxyFactory(new JavassistProxyFactory()) + .beanLoader(new AbstractBeanLoader() { + @Override + public void load(IBeanFactory beanFactory, IBeanFilter filter) throws Exception { + // 手动注册Bean到容器中 + beanFactory.registerBean(DemoBean.class); + } + }).developMode(true).runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Cfgs.class); + owner.registerModule(Logs.class); + owner.registerModule(Servs.class); + // 执行框架初始化 + owner.init(); + // + owner.getBean(DemoBean.class).say(); + // 销毁 + owner.destroy(); + } + +## Beans + +### 包类的自动扫描(AutoScan) + +YMP框架初始化时将自动扫描由`autoscan_packages`参数配置的包路径下所有声明了`@Bean`注解的类文件,首先分析被加载的类所有已实现接口并注册到Bean容器中,然后执行类成员的依赖注入和方法拦截代理的绑定; + +> 说明: +> +> - 相同接口的多个实现类被同时注册到Bean容器时,通过接口获取的实现类将是最后被注册到容器的那个,此时只能通过实例对象类型才能正确获取; +> +> - 若不希望某个类被自动扫描,只需在该类上声明`@Ignored`注解,自动扫描程序都忽略它; + +- 示例一: + + // 业务接口 + public interface IDemo { + String sayHi(); + } + + // 业务接口实现类,单例模式 + @Bean + public class DemoBean implements IDemo { + public String sayHi() { + return "Hello, YMP!"; + } + } + +- 示例二: + + // 示例一中的业务接口实现类,非单例模式 + @Bean(singleton = false) + public class DemoBean implements IDemo { + public String sayHi() { + return "Hello, YMP!"; + } + } + +- 示例三: + + public class DemoBeanHandler implements IBeanHandler { + + @Override + public Object handle(Class targetClass) throws Exception { + // 自定义对象处理逻辑... + return BeanMeta.create(targetClass, true); + } + } + + // 自定义对象处理器 (将取代原来的处理器) + @Bean(handler=DemoBeanHandler.class) + public class DemoBean implements IDemo { + public String sayHi() { + return "Hello, YMP!"; + } + } + +- 示例四: + + // 自定义Bean实例初始化后处理逻辑 + @Bean + public class DemoBean implements IDemo, IBeanInitializer { + public String sayHi() { + return "Hello, YMP!"; + } + + public void afterInitialized() throws Exception { + System.out.println(sayHi() + " ---- afterInitialized."); + } + } + +- 测试代码: + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // 1. 通过接口获取实例对象 + IDemo _demo = YMP.get().getBean(IDemo.class); + System.out.println(_demo.sayHi()); + + // 2. 直接获取实例对象 + _demo = YMP.get().getBean(DemoBean.class); + System.out.println(_demo.sayHi()); + } finally { + YMP.get().destroy(); + } + } + +## 依赖注入(IoC) + +通过在类成员属性上声明`@Inject`和`@By`注解来完成依赖注入的设置,且只有被Bean容器管理的类对象才支持依赖注入,下面举例说明: + +- 示例: + + // 业务接口 + public interface IDemo { + String sayHi(); + } + + // 业务接口实现类1 + @Bean + public class DemoOne implements IDemo { + public String sayHi() { + return "Hello, YMP! I'm DemoOne."; + } + } + + // 业务接口实现类2 + @Bean + public class DemoTwo implements IDemo { + public String sayHi() { + return "Hello, YMP! I'm DemoTwo."; + } + } + +- 测试代码: + + @Bean + public class TestDemo { + + @Inject + private IDemo __demo1; + + @Inject + @By(DemoOne.class) + private IDemo __demo2; + + public void sayHi() { + // _demo1注入的将是最后被注册到容器的IDemo接口实现类 + System.out.println(__demo1.sayHi()); + // _demo2注入的是由@By注解指定的DemoOne类 + System.out.println(__demo2.sayHi()); + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + TestDemo _demo = YMP.get().getBean(TestDemo.class); + _demo.sayHi(); + } finally { + YMP.get().destroy(); + } + } + } + +也可以通过`@Injector`注解声明一个`IBeanInjector`接口实现类向框架注册自定义的注入处理逻辑,下面举例说明如何为注入对象添加包装器: + +- 示例: + + // 定义一个业务接口 + + public interface IInjectBean { + + String getName(); + + void setName(String name); + } + + // 业务接口实现类 + + @Bean + public class InjectBeanImpl implements IInjectBean { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + // 业务对象包装器类 + + public class InjectBeanWrapper implements IInjectBean { + + private IInjectBean __targetBean; + + public InjectBeanWrapper(IInjectBean targetBean) { + __targetBean = targetBean; + } + + public String getName() { + return __targetBean.getName(); + } + + public void setName(String name) { + __targetBean.setName(name); + } + } + + // 自定义一个注解 + + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface Demo { + + String value(); + } + + // 为注解编写自定义注入逻辑 + + @Injector(Demo.class) + public class DemoBeanInjector implements IBeanInjector { + + public Object inject(IBeanFactory beanFactory, Annotation annotation, Class targetClass, Field field, Object originInject) { + // 为从自定义注解取值做准备 + Demo _anno = (Demo) annotation; + if (originInject == null) { + // 若通过@Inject注入的对象不为空则为其赋值 + IInjectBean _bean = new InjectBeanImpl(); + _bean.setName(_anno.value()); + // 创建包装器 + originInject = new InjectBeanWrapper(_bean); + } else { + // 直接创建包装器并赋值 + InjectBeanWrapper _wrapper = new InjectBeanWrapper((IInjectBean) originInject); + _wrapper.setName(_anno.value()); + // + originInject = _wrapper; + } + return originInject; + } + } + +- 测试代码: + + @Bean + public class App { + + @Inject + @Demo("demo") + private IInjectBean __bean; + + public IInjectBean getBean() { + return __bean; + } + + public static void main(String[] args) throws Exception { + try { + YMP.get().init(); + // + App _app = YMP.get().getBean(App.class); + IInjectBean _bean = _app.getBean(); + System.out.println(_bean.getName()); + } finally { + YMP.get().destroy(); + } + } + } + +> 说明: +> +> - 当使用自定义注解进行依赖注入操作时可以忽略`@Inject`注解,若存在则优先执行`@Inject`注入并将此对象当作`IBeanInjector`接口方法参数传入; +> - 当成员变量被声明多个自定义注入规则注解时(不推荐),根据框架加载顺序,仅执行首个注入规则; + +## 方法拦截(AOP) + +YMP框架的AOP是基于CGLIB的MethodInterceptor实现的拦截,通过以下注解进行配置: + +- @Before:用于设置一个类或方法的前置拦截器,声明在类上的前置拦截器将被应用到该类所有方法上; + +- @After:用于设置一个类或方法的后置拦截器,声明在类上的后置拦截器将被应用到该类所有方法上; + +- @Around:用于同时配置一个类或方法的前置和后置拦截器; + +- @Clean:用于清理类上全部或指定的拦截器,被清理的拦截器将不会被执行; + +- @ContextParam:用于设置上下文参数,主要用于向拦截器传递参数配置; + +- @Ignored:声明一个方法将忽略一切拦截器配置; + +> 说明: +> +> - 声明`@Ignored`注解的方法、非公有方法和Object类方法及Object类重载方法将不被拦截器处理。 +> - 使用`@Interceptor`注解声明拦截器类,框架将自动扫描加载并支持IoC依赖注入特性。 + +示例一: + + // 创建自定义拦截器 + public class DemoInterceptor implements IInterceptor { + public Object intercept(InterceptContext context) throws Exception { + // 判断当前拦截器执行方向 + switch (context.getDirection()) { + // 前置 + case BEFORE: + System.out.println("before intercept..."); + // 获取拦截器参数 + String _param = context.getContextParams().get("param"); + if (StringUtils.isNotBlank(_param)) { + System.out.println(_param); + } + break; + // 后置 + case AFTER: + System.out.println("after intercept..."); + } + return null; + } + } + + @Bean + public class TestDemo { + + @Before(DemoInterceptor.class) + public String beforeTest() { + return "前置拦截测试"; + } + + @After(DemoInterceptor.class) + public String afterTest() { + return "后置拦截测试"; + } + + @Around(DemoInterceptor.class) + @ContextParam({ + @ParamItem(key = "param", value = "helloworld") + }) + public String allTest() { + return "拦截器参数传递"; + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + TestDemo _demo = YMP.get().getBean(TestDemo.class); + _demo.beforeTest(); + _demo.afterTest(); + _demo.allTest(); + } finally { + YMP.get().destroy(); + } + } + } + +示例二: + + @Bean + @Before(DemoInterceptor.class) + @ContextParam({ + @ParamItem(key = "param", value = "helloworld") + }) + public class TestDemo { + + public String beforeTest() { + return "默认前置拦截测试"; + } + + @After(DemoInterceptor.class) + public String afterTest() { + return "后置拦截测试"; + } + + @Clean + public String cleanTest() { + return "清理拦截器测试"; + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + TestDemo _demo = YMP.get().getBean(TestDemo.class); + _demo.beforeTest(); + _demo.afterTest(); + _demo.cleanTest(); + } finally { + YMP.get().destroy(); + } + } + } + +**注**:`@ContextParam`注解的value属性允许通过$xxx的格式支持从框架全局参数中获取xxx的值 + +### 包拦截器配置 + +YMP框架支持将`@Before`、`@After`、`@Around`和`@ContextParam`注解在`package-info.java`类中声明,声明后该拦截器配置将作用于其所在包下所有类(子包将继承父级包配置)。 + +拦截器的执行顺序: `package` \> `class` \> `method` + +通过`@Packages`注解让框架自动扫描`package-info.java`类并完成配置注册。 + +示例: + +> 本例将为`net.ymate.demo.controller`包指定拦截器配置,其`package-info.java`内容如下: + + @Packages + @Before(DemoInterceptor.class) + @ContextParam(@ParamItem(key = "param", value = "helloworld")) + package net.ymate.demo.controller; + + import net.ymate.demo.intercept.DemoInterceptor; + import net.ymate.platform.core.beans.annotation.Before; + import net.ymate.platform.core.beans.annotation.ContextParam; + import net.ymate.platform.core.beans.annotation.Packages; + import net.ymate.platform.core.beans.annotation.ParamItem; + +### 拦截器全局规则设置 + +有些时候,我们需要对指定的拦截器或某些类和方法的拦截器配置进行调整,往往我们要修改代码、编译打包并重新部署,这样做显然很麻烦! + +现在我们可以通过配置文件来完成此项工作,配置格式及说明如下: + + #------------------------------------- + # 框架拦截器全局规则设置参数 + #------------------------------------- + + # 是否开启拦截器全局规则设置, 默认为false + ymp.intercept_settings_enabled=true + + # 为指定包配置拦截器, 格式: ymp.intercept.packages.<包名>=<[before:|after:]拦截器类名> (通过'|'分隔多个拦截器) + ymp.intercept.packages.net.ymate.demo.controller=before:net.ymate.demo.intercept.UserSessionInterceptor + + # 全局设置指定的拦截器状态为禁止执行, 仅当取值为disabled时生效, 格式: ymp.intercept.globals.<拦截器类名>=disabled + ymp.intercept.globals.net.ymate.framework.webmvc.intercept.UserSessionAlreadyInterceptor=disabled + + # 为目标类配置拦截器执行规则: + # + # -- 格式: ymp.intercept.settings.<目标类名>#[方法名称]=<[*|before:*|after:*]或[before:|after:]interceptor_class_name[+|-]]> + # -- 假设目标类名称为: net.ymate.demo.controller.DemoController + # + # -- 方式一: 指定目标类所有方法禁止所有拦截器(*表示全部, 即包括前置和后置拦截器) + ymp.intercept.settings.net.ymate.demo.controller.DemoController#=* + + # -- 方式二: 指定目标类的doLogin方法禁止所有前置拦截器(before:表示规则限定为前置拦截器, after:表示规则限定为后置拦截器) + ymp.intercept.settings.net.ymate.demo.controller.DemoController#doLogin=before:* + + # -- 方式三: 指定目标类的doLogout方法禁止某个前置拦截器并增加一个新的后置拦截器(多个执行规则通过'|'分隔, 增加拦截器的'+'可以省略) + ymp.intercept.settings.net.ymate.demo.controller.DemoController#__doLogout=before:net.ymate.demo.intercept.UserSessionInterceptor-|after:net.ymate.demo.intercept.UserStatusUpdateInterceptor+ + +## 记录类属性状态 (PropertyState) + +通过在类成员变量上声明`@PropertyState`注解,并使用`PropertyStateSupport`工具类配合,便可以轻松实现对类成员属性的变化情况进行监控。 + +- @PropertyState注解:声明记录类成员属性值的变化; + + > propertyName:成员属性名称,默认为空则采用当前成员名称; + > + > aliasName:自定义别名,默认为空; + > + > setterName:成员属性SET方法名称,默认为空; + +- 示例代码: + + public class PropertyStateTest { + + @PropertyState(propertyName = "user_name") + private String username; + + @PropertyState(aliasName = "年龄") + private int age; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public static void main(String[] args) throws Exception { + PropertyStateTest _original = new PropertyStateTest(); + _original.setUsername("123456"); + _original.setAge(20); + // + PropertyStateSupport _support = PropertyStateSupport.create(_original); + PropertyStateTest _new = _support.bind(); + _new.setUsername("YMPer"); + _new.setAge(30); + // + System.out.println("发生变更的字段名集合: " + Arrays.asList(_support.getChangedPropertyNames())); + for (PropertyStateSupport.PropertyStateMeta _meta : _support.getChangedProperties()) { + System.out.println("已将" + StringUtils.defaultIfBlank(_meta.getAliasName(), _meta.getPropertyName()) + "由" + _meta.getOriginalValue() + "变更为" + _meta.getNewValue()); + } + } + } + +- 执行结果: + + 发生变更的字段名集合: [user_name, age] + 已将user_name由123456变更为YMPer + 已将年龄由20变更为30 + +## Event + +事件服务,通过事件的注册、订阅和广播完成事件消息的处理,目的是为了减少代码侵入,降低模块之间的业务耦合度,事件消息采用队列存储,采用多线程接口回调实现消息及消息上下文对象的传输,支持同步和异步两种处理模式; + +### 框架事件初始化配置参数 + + #------------------------------------- + # 框架事件初始化参数 + #------------------------------------- + + # 默认事件触发模式(不区分大小写),取值范围:NORMAL-同步执行,ASYNC-异步执行,默认为ASYNC + ymp.event.default_mode= + + # 事件管理提供者接口实现,默认为net.ymate.platform.core.event.impl.DefaultEventProvider + ymp.event.provider_class= + + # 事件线程池初始化大小,默认为Runtime.getRuntime().availableProcessors() + ymp.event.thread_pool_size= + + # 最大线程池大小,默认为 200 + ymp.event.thread_max_pool_size= + + # 线程队列大小,默认为 1024 + ymp.event.thread_queue_size= + +### YMP核心事件对象 + +- ApplicationEvent:框架事件 + + APPLICATION_INITED - 框架初始化 + APPLICATION_DESTROYED - 框架销毁 + +- ModuleEvent:模块事件 + + MODULE_INITED - 模块初始化 + MODULE_DESTROYED - 模块销毁 + +**注**:以上只是YMP框架核心中包含的事件对象,其它模块中包含的事件对象将在其相应的文档描述中阐述; + +### 事件的订阅 + +- 方式一:通过代码手动完成事件的订阅 + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // 订阅模块事件 + YMP.get().getEvents().registerListener(ModuleEvent.class, new IEventListener() { + @Override + public boolean handle(ModuleEvent context) { + switch (context.getEventName()) { + case MODULE_INITED: + // 注意:这段代码是不会被执行的,因为在我们进行事件订阅时,模块的初始化动作已经完成 + System.out.println("Inited :" + context.getSource().getName()); + break; + case MODULE_DESTROYED: + System.out.println("Destroyed :" + context.getSource().getName()); + break; + } + return false; + } + }); + } finally { + YMP.get().destroy(); + } + } + +- 方式二:通过`@EventRegister`注解和IEventRegister接口实现事件的订阅 + + // 首先创建事件注册类,通过实现IEventRegister接口完成事件的订阅 + // 通过@EventRegister注解,该类将在YMP框架初始化时被自动加载 + @EventRegister + public class DemoEventRegister implements IEventRegister { + public void register(Events events) throws Exception { + // 订阅模块事件 + events.registerListener(ModuleEvent.class, new IEventListener() { + @Override + public boolean handle(ModuleEvent context) { + switch (context.getEventName()) { + case MODULE_INITED: + System.out.println("Inited :" + context.getSource().getName()); + break; + case MODULE_DESTROYED: + System.out.println("Destroyed :" + context.getSource().getName()); + break; + } + return false; + } + }); + // + // ... 还可以添加更多的事件订阅代码 + } + } + + // 框架启动测试 + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // Do Nothing... + } finally { + YMP.get().destroy(); + } + } + +### 自定义事件 + +YMP的事件对象必须实现IEvent接口的同时需要继承EventContext对象,下面的代码就是一个自定义事件对象: + +- 创建自定义事件对象 + + public class DemoEvent extends EventContext implements IEvent { + + public enum EVENT { + CUSTOM_EVENT_ONE, CUSTOM_EVENT_TWO + } + + public DemoEvent(Object owner, Class eventClass, EVENT eventName) { + super(owner, eventClass, eventName); + } + } + + 说明:EventContext的注解中的第一个参数代表事件源对象类型,第二个参数是指定用于事件监听事件名称的枚举类型; + +- 注册自定义事件 + + - 方式一:通过代码注册 + + YMP.get().getEvents().registerEvent(DemoEvent.class); + + - 方式二:通过在自定义事件类上声明`@Event`注解,框架初始化时将被自动注册; + +- 订阅自定义事件 + + 事件订阅(或监听)需实现IEventListener接口,该接口的handle方法返回值在同步触发模式下将影响事件监听队列是否终止执行,异步触发模式下请忽略此返回值; + + // 采用默认模式执行事件监听器 + YMP.get().getEvents().registerListener(DemoEvent.class, new IEventListener() { + + public boolean handle(DemoEvent context) { + switch (context.getEventName()) { + case CUSTOM_EVENT_ONE: + System.out.println("CUSTOM_EVENT_ONE"); + break; + case CUSTOM_EVENT_TWO: + System.out.println("CUSTOM_EVENT_TWO"); + break; + } + return false; + } + }); + + // 采用异步模式执行事件监听器 + YMP.get().getEvents().registerListener(Events.MODE.ASYNC, DemoEvent.class, new IEventListener() { + + public boolean handle(DemoEvent context) { + ...... + } + }); + + 当然,也可以通过`@EventRegister`注解和IEventRegister接口实现自定义事件的订阅; + + **注**:当某个事件被触发后,订阅(或监听)该事件的接口被回调执行的顺序是不能被保证的; + +- 触发自定义事件 + + YMP.get().getEvents().fireEvent(new DemoEvent(YMP.get(), DemoEvent.class, DemoEvent.EVENT.CUSTOM_EVENT_ONE)); + // + YMP.get().getEvents().fireEvent(new DemoEvent(YMP.get(), DemoEvent.class, DemoEvent.EVENT.CUSTOM_EVENT_TWO)); + +- 示例测试代码: + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // 注册自定义事件对象 + YMP.get().getEvents().registerEvent(DemoEvent.class); + // 注册自定义事件监听 + YMP.get().getEvents().registerListener(DemoEvent.class, new IEventListener() { + + public boolean handle(DemoEvent context) { + switch (context.getEventName()) { + case CUSTOM_EVENT_ONE: + System.out.println("CUSTOM_EVENT_ONE"); + break; + case CUSTOM_EVENT_TWO: + System.out.println("CUSTOM_EVENT_TWO"); + break; + } + return false; + } + }); + // 触发事件 + YMP.get().getEvents().fireEvent(new DemoEvent(YMP.get(), DemoEvent.class, DemoEvent.EVENT.CUSTOM_EVENT_ONE)); + YMP.get().getEvents().fireEvent(new DemoEvent(YMP.get(), DemoEvent.class, DemoEvent.EVENT.CUSTOM_EVENT_TWO)); + } finally { + YMP.get().destroy(); + } + } + + +## Module + +### 创建自定义模块 + +- 步骤一:根据业务需求创建需要对外暴露的业务接口 + + public interface IDemoModule { + + // 为方便引用,定义模块名称常量 + String MODULE_NAME = "demomodule"; + + // 返回自定义模块的参数配置接口对象 + IDemoModuleCfg getModuleCfg(); + + // 对外暴露的业务方法 + String sayHi(); + } + +- 步骤二:处理自定义模块的配置参数,下列代码假定测试模块有两个自定义参数 + + // 定义模块配置接口 + public interface IDemoModuleCfg { + + String getModuleParamOne(); + + String getModuleParamTwo(); + } + + // 实现模块配置接口 + public class DemoModuleCfg implements IDemoModuleCfg { + + private String __moduleParamOne; + + private String __moduleParamTwo; + + public DemoModuleCfg(YMP owner) { + // 从YMP框架中获取模块配置映射 + Map _moduleCfgs = owner.getConfig().getModuleConfigs(IDemoModule.MODULE_NAME); + // + __moduleParamOne = _moduleCfgs.get("module_param_one"); + __moduleParamTwo = _moduleCfgs.get("module_param_two"); + } + + public String getModuleParamOne() { + return __moduleParamOne; + } + + public String getModuleParamTwo() { + return __moduleParamTwo; + } + } + +- 步骤三:实现模块及业务接口 + + **注**:一定不要忘记在模块实现类上声明`@Module`注解,这样才能被YMP框架自动扫描、加载并初始化; + + @Module + public class DemoModule implements IModule, IDemoModule { + + private YMP __owner; + + private IDemoModuleCfg __moduleCfg; + + private boolean __inited; + + public String getName() { + return IDemoModule.MODULE_NAME; + } + + public void init(YMP owner) throws Exception { + if (!__inited) { + __owner = owner; + __moduleCfg = new DemoModuleCfg(owner); + // + __inited = true; + } + } + + public boolean isInited() { + return __inited; + } + + public YMP getOwner() { + return __owner; + } + + public IDemoModuleCfg getModuleCfg() { + return __moduleCfg; + } + + public void destroy() throws Exception { + if (__inited) { + __inited = false; + // + __moduleCfg = null; + __owner = null; + } + } + + public String sayHi() { + return "Hi, YMP!"; + } + } + +- 步骤四:在YMP的配置文件ymp-conf.properties中添加模块的配置内容 + + 格式: ymp.configs.<模块名称>.<参数名称>=[参数值] + + ymp.configs.demomodule.module_param_one=module_param_one_value + ymp.configs.demomodule.module_param_two=module_param_two_value + + +### 调用自定义模块 + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + // 获取自定义模块实例对象 + IDemoModule _demoModule = YMP.get().getModule(IDemoModule.class); + // 调用模块业务接口方法 + System.out.println(_demoModule.sayHi()); + // 调用模块配置信息 + System.out.println(_demoModule.getModuleCfg().getModuleParamOne()); + } finally { + YMP.get().destroy(); + } + } + +**注**:自定义模块不支持IoC、AOP等特性; + +## I18N + +I18N服务是在YMP框架启动时初始化,其根据ymp.i18n_default_locale进行语言配置,默认采用系统运行环境的语言设置; + + +- 国际化资源管理器提供的主要方法: + + + 获取当前语言设置 + + I18N.current(); + + + 设置当前语言 + + // 变更当前语言设置且不触发事件 + I18N.current(Locale.ENGLISH); + + 或者 + + // 将触发监听处理器onChanged事件 + I18N.change(Locale.ENGLISH); + + + 根据当前语言设置,加载指定名称资源文件内指定的属性值 + + I18N.load("resources", "home_title"); + + 或者 + + I18N.load("resources", "home_title", "首页"); + + + 格式化消息字符串并绑定参数 + + // 加载指定名称资源文件内指定的属性并使用格式化参数绑定 + I18N.formatMessage("resources", "site_title", "Welcome {0}, {1}","YMP",“GoodLuck!”); + + // 使用格式化参数绑定 + I18N.formatMsg("Hello, {0}, {1}", "YMP",“GoodLuck!”); + +- 国际化资源管理器事件监听处理器,通过实现II18NEventHandler接口,在YMP配置文件中的`i18n_event_handler_class`参数进行设置,该监听器可以完成如下操作: + + + 自定义资源文件加载过程 + + + 自定义获取当前语言设置 + + + 语言设置变更的事件处理过程 + +## Lang + +### BlurObject:模糊对象 + + BlurObject.bind("1234").toLongValue(); + +### PairObject:结对对象 + + List _key = new ArrayList(); + Map _value = new HashMap(); + ... + PairObject _pObj = new PairObject(_key, _value); + + // + _pObj.getKey(); + // + _pObj.getValue(); + +### TreeObject:树型对象 + + Object _id = UUIDUtils.UUID(); + TreeObject _target = new TreeObject() + .put("id", _id) + .put("category", new Byte[]{1, 2, 3, 4}) + .put("create_time", new Date().getTime(), true) + .put("is_locked", true) + .put("detail", new TreeObject() + .put("real_name", "汉字将被混淆", true) + .put("age", 32)); + + // 这样赋值是List + TreeObject _list = new TreeObject(); + _list.add("list item 1"); + _list.add("list item 2"); + + // 这样赋值代表Map + TreeObject _map = new TreeObject(); + _map.put("key1", "keyvalue1"); + _map.put("key2", "keyvalue2"); + + TreeObject idsT = new TreeObject(); + idsT.put("ids", _list); + idsT.put("maps", _map); + + // List操作 + System.out.println(idsT.get("ids").isList()); + System.out.println(idsT.get("ids").getList()); + + // Map操作 + System.out.println(idsT.get("maps").isMap()); + System.out.println(idsT.get("maps").getMap()); + + // + _target.put("map", _map); + _target.put("list", _list); + + // + System.out.println(_target.get("detail").getMixString("real_name")); + + // TreeObject对象转换为JSON字符串输出 + String _jsonStr = _target.toJson().toJSONString(); + System.out.println(_jsonStr); + + // 通过JSON字符串转换为TreeObject对象-->再转为JSON字符串输出 + String _jsonStrTmp = (_target = TreeObject.fromJson(_target.toJson())).toJson().toJSONString(); + System.out.println(_jsonStrTmp); + System.out.println(_jsonStr.equals(_jsonStrTmp)); + +## Util + +关于YMP框架常用的工具类,这里着重介绍以下几个: + +- ClassUtils提供的BeanWrapper工具,它是一个类对象包裹器,赋予对象简单的属性操作能力; + + public static void main(String[] args) throws Exception { + // 包裹一个Bean对象 + ClassUtils.BeanWrapper _w = ClassUtils.wrapper(new DemoBean()); + // 输出该对象的成员属性名称 + for (String _fieldName : _w.getFieldNames()) { + System.out.println(_fieldName); + } + // 为成员属性设置值 + _w.setValue("name", "YMP"); + // 获取成员属性值 + _w.getValue("name"); + // 拷贝Bean对象属性到目标对象(不局限相同对象) + DemoBean _bean = _w.duplicate(new DemoBean()); + // 将对象属性转为Map存储 + Map _maps = _w.toMap(); + // 通过Map对象构建Bean对象并获取Bean实例 + DemoBean _target = ClassUtils.wrapper(DemoBean.class).fromMap(_maps).getTargetObject(); + } + +- RuntimeUtils运行时工具类,获取运行时相关信息; + + + 获取当前环境变量: + + RuntimeUtils.getSystemEnvs(); + + RuntimeUtils.getSystemEnv("JAVA_HOME"); + + + 判断当前运行环境操作系统: + + RuntimeUtils.isUnixOrLinux(); + + RuntimeUtils.isWindows(); + + + 获取应用根路径:若WEB工程则基于.../WEB-INF/返回,若普通工程则返回类所在路径 + + RuntimeUtils.getRootPath(); + + RuntimeUtils.getRootPath(false); + + + 替换环境变量:支持${root}、${user.dir}和${user.home}环境变量占位符替换 + + RuntimeUtils.replaceEnvVariable("${root}/home"); diff --git a/misc/site/guide/log.md b/misc/site/guide/log.md new file mode 100755 index 00000000..67dddf6b --- /dev/null +++ b/misc/site/guide/log.md @@ -0,0 +1,245 @@ +--- +sidebarDepth: 2 +--- + +# 日志(Log) + +基于开源日志框架Log4J 2实现,提供对日志记录器对象的统一管理,可以在任意位置调用任意日志记录器输出日志,实现系统与业务日志的分离;与YMP配置体系模块配合使用,效果更佳:) + +日志框架同时提供扩展支持的两个子项目: + +> - log-jcl:用于整合apache-commons-logging日志框架; +> - log-slf4j:针对slf4j日志系统提供支持; + +## Maven包依赖 + +- Log依赖配置 + + + net.ymate.platform + ymate-platform-log + + + +- log-jcl依赖配置 + + + net.ymate.platform + ymate-platform-log-jcl + + + +- log-slf4j依赖配置 + + + net.ymate.platform + ymate-platform-log-slf4j + + + +> **注**: +> - 请根据您项目的实际情况,在项目的pom.xml中添加相应配置,该模块已经默认引入核心包依赖,无需重复配置。 +> - 在使用中需要注意`log-jcl`和`log-slf4j`内部使用的YMP对象是全局实例(采用`YMP.get()`方式获取的YMP实例对象称为全局实例)。 + +## 模块事件 + +LogEvent事件枚举对象包括以下事件类型: + +|事务类型|说明| +|---|---| +|LOG_WRITE_IN|日志写入时触发该事件| + +## 模块配置 + +日志模块初始化参数, 将下列配置项按需添加到ymp-conf.properties文件中, 否则模块将使用默认配置进行初始化: + + #------------------------------------- + # 日志模块初始化参数 + #------------------------------------- + + # 日志记录器配置文件,默认为${root}/cfgs/log4j.xml,变量${user.dir}的取值结果将受配置体系模块影响 + ymp.configs.log.config_file= + + # 日志文件输出路径,默认为${root}/logs/ + ymp.configs.log.output_dir= + + # 日志记录器默认名称,默认为default + ymp.configs.log.logger_name= + + # 日志记录器接口实现类,默认为net.ymate.platform.log.impl.DefaultLogger + ymp.configs.log.logger_class= + + # 日志记录器是否允许控制台输出,默认为false + ymp.configs.log.allow_output_console= + + # 日志记录器是否采用简化包名输出,默认为false + ymp.configs.log.simplified_package_name= + + # 日志记录器是否采用格式化填充输出,默认为false + ymp.configs.log.format_padded_output= + + > **注**:需要注意`config_file`配置的log4j.xml文件是否存在,以及`output_dir`指定的输出路径是否正确有效,这两项配置会影响YMP框架启动时异常; + > + > 此外,建议在开发阶段将`allow_output_console`参数设置为true,这样可以通过控制台直接查看日志输出; + + +Log4J配置文件,内容如下: + + + + + + + + + + + + + + + + + + + + + **注**:该文件应根据ymp.configs.log.config_file指定的位置,其内容请根据实际情况调整。 + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置日志模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + LogModuleConfigurable.create() + .configFile("${root}/cfgs/log4j.xml") + .outputDir("${root}/logs/") + .loggerName("default") + .allowOutputConsole(true)).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Logs.class); + // 执行框架初始化 + owner.init(); + +## 使用示例 + +首先,为了配合演示多个日志记录器的使用方法,修改log4j.xml配置内容如下: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +> 上面的配置文件中共配置两个日志记录器: +> +> - default:默认根日志记录器,它将记录所有日志内容; +> - wechat:自定义的日志记录器; + +示例代码: + +- 使用默认日志记录器输出: + + Logs.get().getLogger().debug("日志将被输出到default.log文件..."); + Logs.get().getLogger().debug("日志内容", e); + + > **注**:默认日志记录器是由`logger_name`参数指定的,默认值为default; + +- 输出日志到wechat.log文件中: + + ILogger _wechat = Logs.get().getLogger("wechat"); + _wechat.debug("日志将被分别输出到wechat.log和default.log文件中"); + // + if (_wechat.isDebugEnabled()) { + _wechat.debug("日志内容", e); + } + + // 或者 + Logs.get().getLogger("wechat").info("日志内容"); + +## 怀旧版业务日志记录工具使用示例 + +通过`@Loggable`注解与`Logoo`类配合来记录完整业务逻辑处理过程; + +- @Loggable: 声明一个类对业务日志记录工具的支持或声明一个方法开始业务日志记录器: + + > value[]:设定输出到日志记录器名称集合,可选; + > + > flag:自定义标识; + > + > action:自定义动作标识; + > + > level:日志输出级别, 默认为: INFO; + > + > merge:是否日志合并输出, 默认为: false; + > + > adapterClass:自定义日志适配器类; + +- 示例代码:: + + public class DemoLogooAdapter extends DefaultLogooAdapter { + + @Override + public void onLogWritten(String flag, String action, Map attributes) { + // 自定义业务逻辑处理过程中传递的参数, 如: 写入数据库等 + System.out.println("业务逻辑处理完毕..."); + } + } + + @Controller + @RequestMapping("/hello") + @Loggable(value = "custom", flag = "示例:") + public class HelloController { + + @RequestMapping("/") + @Loggable(flag = "逻辑处理", merge = true, adapterClass = DemoLogooAdapter.class) + public IView hello() throws Exception { + // 输出日志 + Logoo.log("日志输出..."); + // 传递参数 + Logoo.addAttribute("key1", "value1); + // + Logoo.finished(); + // + return View.textView("Hello YMP world!"); + } + } diff --git a/misc/site/guide/persistence/README.md b/misc/site/guide/persistence/README.md new file mode 100755 index 00000000..1ec1a171 --- /dev/null +++ b/misc/site/guide/persistence/README.md @@ -0,0 +1,5 @@ +# 持久化 (Persistence) + +- [JDBC](./jdbc.md) +- [MongoDB](./mongodb.md) +- [Redis](./redis.md) \ No newline at end of file diff --git a/misc/site/guide/persistence/jdbc.md b/misc/site/guide/persistence/jdbc.md new file mode 100755 index 00000000..4be7ee38 --- /dev/null +++ b/misc/site/guide/persistence/jdbc.md @@ -0,0 +1,1551 @@ +--- +sidebarDepth: 2 +--- + +# JDBC + +JDBC持久化模块针对关系型数据库(RDBMS)数据存取的一套简单解决方案,主要关注数据存取的效率、易用性和透明,其具备以下功能特征: + +- 基于JDBC框架API进行轻量封装,结构简单、便于开发、调试和维护; +- 优化批量数据更新、标准化结果集、预编译SQL语句处理; +- 支持单实体ORM操作,无需编写SQL语句; +- 提供脚手架工具,快速生成数据实体类,支持链式调用; +- 支持通过存储器注解自定义SQL语句或从配置文件中加载SQL并自动执行; +- 支持结果集与值对象的自动装配,支持自定义装配规则; +- 支持多数据源,默认支持C3P0、DBCP、JNDI连接池配置,支持数据源扩展; +- 支持多种数据库(如:Oracle、MySQL、SQLServer、SQLite、H2、PostgreSQL等); +- 支持面向对象的数据库查询封装,有助于减少或降低程序编译期错误; +- 支持数据库事务嵌套; +- 支持数据库视图和存储过程; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-persistence-jdbc + + + +> **注**:在项目的pom.xml中添加上述配置,该模块已经默认引入核心包及持久化基础包依赖,无需重复配置。 + +## 模块初始化配置 + + #------------------------------------- + # JDBC持久化模块初始化参数 + #------------------------------------- + + # 默认数据源名称,默认值为default + ymp.configs.persistence.jdbc.ds_default_name= + + # 数据源列表,多个数据源名称间用'|'分隔,默认为default + ymp.configs.persistence.jdbc.ds_name_list= + + # 是否显示执行的SQL语句,默认为false + ymp.configs.persistence.jdbc.ds.default.show_sql= + + # 是否开启堆栈跟踪,默认为false + ymp.configs.persistence.jdbc.ds.default.stack_traces= + + # 堆栈跟踪层级深度,默认为0(即全部) + ymp.configs.persistence.jdbc.ds.default.stack_trace_depth= + + # 堆栈跟踪包名前缀过滤,默认为空 + ymp.configs.persistence.jdbc.ds.default.stack_trace_package= + + # 数据库表前缀名称,默认为空 + ymp.configs.persistence.jdbc.ds.default.table_prefix= + + # 数据源适配器,可选值为已知适配器名称或自定义适配置类名称,默认为default,目前支持已知适配器[default|dbcp|c3p0|jndi|...] + ymp.configs.persistence.jdbc.ds.default.adapter_class= + + # 数据库类型,可选参数,默认值将通过连接字符串分析获得,目前支持[mysql|oracle|sqlserver|db2|sqlite|postgresql|hsqldb|h2] + ymp.configs.persistence.jdbc.ds.default.type= + + # 数据库方言,可选参数,自定义方言将覆盖默认配置 + ymp.configs.persistence.jdbc.ds.default.dialect_class= + + # 数据库引用标识符,默认为空 + ymp.configs.persistence.jdbc.ds.default.identifier_quote= + + # 数据库连接驱动,可选参数,框架默认将根据数据库类型进行自动匹配 + ymp.configs.persistence.jdbc.ds.default.driver_class= + + # 数据库连接字符串,必填参数 + ymp.configs.persistence.jdbc.ds.default.connection_url= + + # 数据库访问用户名称,必填参数 + ymp.configs.persistence.jdbc.ds.default.username= + + # 数据库访问密码,可选参数,经过默认密码处理器加密后的admin字符串为wRI2rASW58E + ymp.configs.persistence.jdbc.ds.default.password= + + # 数据库访问密码是否已加密,默认为false + ymp.configs.persistence.jdbc.ds.default.password_encrypted= + + # 数据库密码处理器,可选参数,用于对已加密数据库访问密码进行解密,默认为空 + ymp.configs.persistence.jdbc.ds.default.password_class= + +配置参数补充说明: + +> 数据源的数据库连接字符串和用户名是必填项,其它均为可选参数,最简配置如下: +> +> >ymp.configs.persistence.jdbc.ds.default.connection_url=jdbc:mysql://localhost:3306/mydb +> > +> >ymp.configs.persistence.jdbc.ds.default.username=root +> +> 为了避免明文密码出现在配置文件中,YMP框架提供了默认的数据库密码处理器,或者通过IPasswordProcessor接口自行实现; +> +> >net.ymate.platform.core.support.impl.DefaultPasswordProcessor + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置JDBC模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + DatabaseModuleConfigurable.create().defaultDataSourceName("default").addDataSource( + DataSourceConfigurable.create("default") + .connectionUrl("jdbc:mysql://localhost:3306/database_name?useUnicode=true&characterEncoding=UTF-8") + .username("root") + .password("wRI2rASW58E") + .passwordEncrypted(true) + .adapterClass("c3p0") + .tablePrefix("ym_") + .showSql(true) + .stackTraces(true))).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(JDBC.class); + // 执行框架初始化 + owner.init(); + +## 数据源(DataSource) + +### 多数据源连接 + +JDBC持久化模块默认支持多数据源配置,下面通过简单的配置来展示如何连接多个数据库: + + # 定义两个数据源分别用于连接MySQL和Oracle数据库,同时指定默认数据源为default(即MySQL数据库) + ymp.configs.persistence.jdbc.ds_default_name=default + ymp.configs.persistence.jdbc.ds_name_list=default|oracledb + + # 连接到MySQL数据库的数据源配置 + ymp.configs.persistence.jdbc.ds.default.connection_url=jdbc:mysql://localhost:3306/mydb + ymp.configs.persistence.jdbc.ds.default.username=root + ymp.configs.persistence.jdbc.ds.default.password=123456 + + # 连接到Oracle数据库的数据源配置 + ymp.configs.persistence.jdbc.ds.oracledb.connection_url=jdbc:oracle:thin:@localhost:1521:ORCL + ymp.configs.persistence.jdbc.ds.oracledb.username=ORCL + ymp.configs.persistence.jdbc.ds.oracledb.password=123456 + +从上述配置中可以看出,配置不同的数据源时只需要定义数据源名称列表,再根据列表逐一配置即可; + +### 连接池配置 + +JDBC持久化模块提供的数据源类型如下: + +- default:默认数据源适配器,通过DriverManager直接连接数据库,建议仅用于测试; +- c3p0:基于C3P0连接池的数据源适配器; +- dbcp:基于DBCP连接池的数据源适配器; +- jndi:基于JNDI的数据源适配器; + +只需根据实际情况调整对应数据源名称的配置,如: + + ymp.configs.persistence.jdbc.ds.default.adapter_class=dbcp + +针对于dbcp和c3p0连接池的配置文件及内容,请将对应的dbcp.properties或c3p0.properties文件放置在工程的classpath根路径下,配置内容请参看JDBC持久化模块开源工程中的示例文件; + +另外,dbcp连接池支持根据数据源名称进行单独配置(如:`dbcp_oracledb.properties`,此文件将优先于`dbcp.properties`被加载); + +当然,也可以通过IDataSourceAdapter接口自行实现,框架针对IDataSourceAdapter接口提供了一个抽象封装AbstractDataSourceAdapter类,直接继承即可; + +### 数据库连接持有者(IConnectionHolder) + +用于记录真正的数据库连接对象(Connection)原始的状态及与数据源对应关系; + +## 数据实体(Entity) + +### 数据实体注解 + +- @Entity:声明一个类为数据实体对象; + + > value:实体名称(数据库表名称),默认采用当前类名称; + + @Entity("tb_demo") + public class Demo { + //... + } + +- @Id:声明一个类成员为主键; + + > 无参数,配合@Property注解使用; + + @Entity("tb_demo") + public class Demo { + + @Id + @Property + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + +- @Property:声明一个类成员为数据实体属性; + + > name:实现属性名称,默认采用当前成员名称; + > + > autoincrement:是否为自动增长,默认为false; + > + > sequenceName:序列名称,适用于类似Oracle等数据库,配合autoincrement参数一同使用; + > + > nullable:允许为空,默认为true; + > + > unsigned:是否为无符号,默认为false; + > + > length:数据长度,默认0为不限制; + > + > decimals:小数位数,默认0为无小数; + > + > type:数据类型,默认为Type.FIELD.VARCHAR; + + @Entity("tb_user") + public class User { + + @Id + @Property + private String id; + + @Property(name = "user_name", + nullable = false, + length = 32) + private String username; + + @Property(name = "age", + unsigned = true, + type = Type.FIELD.INT) + private Integer age; + + // 省略Get/Set方法... + } + +- @PK:声明一个类为某数据实体的复合主键对象; + + > 无参数; + + @PK + public class UserExtPK { + + @Property + private String uid; + + @Property(name = "wx_id") + private String wxId; + + // 省略Get/Set方法... + } + + @Entity("tb_user_ext") + public class UserExt { + + @Id + private UserExtPK id; + + @Property(name = "open_id", + nullable = false, + length = 32) + private String openId; + + // 省略Get/Set方法... + } + +- @Readonly:声明一个成员为只读属性,数据实体更新时其将被忽略; + + > 无参数,配合@Property注解使用; + + @Entity("tb_demo") + public class Demo { + + @Id + @Property + private String id; + + @Property(name = "create_time") + @Readonly + private Date createTime; + + // 省略Get/Set方法... + } + +- @Indexes:声明一组数据实体的索引; + +- @Index:声明一个数据实体的索引; + +- @Comment:注释内容; + +- @Default:为一个成员属性或方法参数指定默认值; + +看着这么多的注解,是不是觉得编写实体很麻烦呢,不要急,框架提供了自动生成实体的方法,往下看:) + +**注**:上面注解或注解参数中有一些是用于未来能通过实体对象直接创建数据库表结构(以及SQL脚本文件)的,可以暂时忽略; + +### 自动生成实体类 + +YMP框架自v1.0开始就支持通过数据库表结构自动生成实体类代码,所以v2.0版本不但重构了实体代码生成器,而且更简单好用! + + #------------------------------------- + # JDBC数据实体代码生成器配置参数 + #------------------------------------- + + # 是否生成新的BaseEntity类,默认为false(即表示使用框架提供的BaseEntity类) + ymp.params.jdbc.use_base_entity= + + # 是否使用类名后缀,不使用和使用的区别如: User-->UserModel,默认为false + ymp.params.jdbc.use_class_suffix= + + # 是否采用链式调用模式,默认为false + ymp.params.jdbc.use_chain_mode= + + # 是否添加类成员属性值状态变化注解,默认为false + ymp.params.jdbc.use_state_support= + + # 实体及属性命名过滤器接口实现类,默认为空 + ymp.params.jdbc.named_filter_class= + + # 数据库名称(仅针对特定的数据库使用,如Oracle),默认为空 + ymp.params.jdbc.db_name= + + # 数据库用户名称(仅针对特定的数据库使用,如Oracle),默认为空 + ymp.params.jdbc.db_username= + + # 数据库表名称前缀,多个用'|'分隔,默认为空 + ymp.params.jdbc.table_prefix= + + # 否剔除生成的实体映射表名前缀,默认为false + ymp.params.jdbc.remove_table_prefix= + + # 预生成实体的数据表名称列表,多个用'|'分隔,默认为空表示全部生成 + ymp.params.jdbc.table_list= + + # 排除的数据表名称列表,在此列表内的数据表将不被生成实体,多个用'|'分隔,默认为空 + ymp.params.jdbc.table_exclude_list= + + # 需要添加@Readonly注解声明的字段名称列表,多个用'|'分隔,默认为空 + ymp.params.jdbc.readonly_field_list= + + # 生成的代码文件输出路径,默认为${root} + ymp.params.jdbc.output_path= + + # 生成的代码所属包名称,默认为: packages + ymp.params.jdbc.package_name= + +实际上你可以什么都不用配置(请参看以上配置项说明,根据实际情况进行配置),但使用过程中需要注意以下几点: + +> - 代码生成器依赖JDBC持久化模块才能完成与数据库连接等操作; +> +> - 在多数据源模式下,代码生成器使用的是默认数据源; +> +> - 代码生成器依赖`freemarker`模板引擎,所以请检查依赖关系是否正确; +> +> - 在WEB工程中运行代码生成器时请确认`servlet-api`和`jsp-api`包依赖关系是否正确; +> +> - 如果你的工程中引用了很多的模块,在运行代码生成器时可以暂时通过ymp.excluded_modules参数排除掉; +> +> - 如果使用的JDBC驱动是`mysql-connector-java-6.x`及以上版本时,则必须配置`db_name`和`db_username`参数; +> +> - 实体及属性命名过滤器参数`named_filter_class`指定的类需要实现`IEntityNamedFilter`接口; + +了解了以上的配置后,直接运行代码生成器: + + net.ymate.platform.persistence.jdbc.scaffold.EntityGenerator + +找到并运行它,如果是Maven项目,可以通过以下命令执执行: + + mvn compile exec:java -Dexec.mainClass="net.ymate.platform.persistence.jdbc.scaffold.EntityGenerator" + +OK!就这么简单,一切都结束了! + +当然,上面介绍的实体生成方法还是有些麻烦,所以我们提供了另外一种更方便的方式 —— 通过YMP框架提供的Maven扩展工具生成实体: + +- 步骤1:编译并安装`ymate-maven-extension`扩展工具 + + - 下载YMP框架Maven扩展工具源码([点击这里查看此项目](https://gitee.com/suninformation/ymate-maven-extension)) + + 执行命令: + + git clone https://gitee.com/suninformation/ymate-maven-extension.git + + - 编译并安装到本地Maven仓库 + + 执行命令: + + cd ymate-maven-extension + mvn clean install + +- 步骤2:将pom.xml中添加`ymate-maven-plugin`插件 + + + net.ymate.maven.plugins + ymate-maven-plugin + 1.0-SNAPSHOT + + +- 步骤3:执行插件生成实体 + + 在工程根路径下执行命令: + + mvn ymate:entity + + 输出内容: + + Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 + [INFO] Scanning for projects... + [INFO] ......(此处省略若干字) + [INFO] --- ymate-maven-plugin:1.0-SNAPSHOT:entity (default-cli) @ ymp-examples-webapp --- + 三月 25, 2016 12:25:07 上午 net.ymate.platform.core.YMP init + 信息: + __ ____ __ ____ ____ + \ \ / / \/ | _ \ __ _|___ \ + \ V /| |\/| | |_) | \ \ / / __) | + | | | | | | __/ \ V / / __/ + |_| |_| |_|_| \_/ |_____| Website: http://www.ymate.net/ + 三月 25, 2016 12:25:07 上午 net.ymate.platform.core.YMP init + 信息: Initializing ymate-platform-core-2.0.0-GA build-20160324-2339 - debug:true + ......(此处省略若干字) + 信息: [show tables][][1][13ms] + Output file "/Users/suninformation/IdeaProjects/ymate-platform-examples/ymp-examples-webapp/src/main/java/net/ymate/platform/examples/model/User.java". + [INFO] ------------------------------------------------------------------------ + [INFO] BUILD SUCCESS + [INFO] ------------------------------------------------------------------------ + [INFO] Total time: 1.577s + [INFO] Finished at: Fri Mar 25 00:25:08 CST 2016 + [INFO] Final Memory: 10M/163M + [INFO] ------------------------------------------------------------------------ + + 通过插件生成的代码默认放置在`src/main/java`路径,当数据库表发生变化时,直接执行插件命令就可以快速更新数据实体对象,**是不是很更方便呢,大家可以动手尝试一下!**:p + +## 事务(Transaction) + +基于YMPv2.0的新特性,JDBC模块对数据库事务的处理更加灵活,任何被类对象管理器管理的对象都可以通过@Transaction注解支持事务; + +- @Transaction注解: + + 参数说明: + + > value:事务类型(参考JDBC事务类型),默认为JDBC.TRANSACTION.READ_COMMITTED; + + + 使用方式: + + > 首先,需要数据库事务支持的类对象必须声明@Transaction注解; + > + > 然后,在具体需要开启事务处理的类方法上添加@Transaction注解; + +- 事务示例代码: + + public interface IUserService { + + User doGetUser(String username, String pwd); + + boolean doLogin(String username, String pwd); + } + + @Bean + @Transaction + public class UserService implements IUserService { + + public User doGetUser(final String username, final String pwd) { + return JDBC.get().openSession(new ISessionExecutor() { + public User execute(ISession session) throws Exception { + Cond _cond = Cond.create().eq("username").param(username).and().eq("pwd").param(pwd); + return session.findFirst(EntitySQL.create(User.class), Where.create(_cond)); + } + }); + } + + @Transaction + public boolean doLogin(String username, String pwd) { + User _user = doGetUser(username, pwd); + if (_user != null) { + _user.setLastLoginTime(System.currentTimeMillis()); + _user.update(); + // + return true; + } + return false; + } + } + + @Bean + public class TransDemo { + + @Inject + private IUserService __userService; + + public boolean testTrans() { + return __userService.doLogin("suninformation", "123456"); + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + TransDemo _demo = YMP.get().getBean(TransDemo.class); + _demo.testTrans(); + } finally { + YMP.get().destroy(); + } + } + } + +## 会话(Session) + +会话是对应用中具体业务操作触发的一系列与数据库之间的交互过程的封装,通过建立一个临时通道,负责与数据库之间连接资源的创建及回收,同时提供更为高级的抽象指令接口调用,基于会话的优点: + +> 开发人员不需要担心连接资源是否正确释放; +> +> 严格的编码规范更利于维护和理解; +> +> 更好的业务封装性; + +- 会话对象参数: + + + 数据库连接持有者(IConnectionHolder): + + 指定本次会话使用的数据源连接; + + + 会话执行器(ISessionExecutor): + + 以内部类的形式定义本次会话返回结果对象并提供Session实例对象的引用; + + +- 开启会话示例代码: + + // 使用默认数据源开启会话 + User _result = JDBC.get().openSession(new ISessionExecutor() { + public User execute(ISession session) throws Exception { + // TODO 此处填写业务逻辑代码 + return session.findFirst(EntitySQL.create(User.class)); + } + }); + + // 使用指定的数据源开启会话 + IConnectionHolder _conn = JDBC.get().getConnectionHolder("oracledb"); + // 不需要关心_conn对象的资源释放 + IResultSet _results = JDBC.get().openSession(_conn, new ISessionExecutor>() { + public IResultSet execute(ISession session) throws Exception { + // TODO 此处填写业务逻辑代码 + return session.find(EntitySQL.create(User.class)); + } + }); + +- 基于ISession接口的数据库操作: + + 示例代码是围绕用户(User)数据实体完成的CRUD(新增、查询、修改、删除)操作来展示如何使用ISession对象,数据实体如下: + + @Entity("user") + public static class User extends BaseEntity { + + @Id + @Property + private String id; + + @Property(name = "user_name") + private String username; + + @Property(name = "pwd") + private String pwd; + + @Property(name = "sex") + private String sex; + + @Property(name = "age") + private Integer age; + + // 忽略Getter和Setter方法 + + public static class FIELDS { + public static final String ID = "id"; + public static final String USER_NAME = "username"; + public static final String PWD = "pwd"; + public static final String SEX = "sex"; + public static final String AGE = "age"; + } + public static final String TABLE_NAME = "user"; + } + + + 插入(Insert): + + User _user = new User(); + _user.setId(UUIDUtils.UUID()); + _user.setUsername("suninformation"); + _user.setPwd(DigestUtils.md5Hex("123456")); + _user.setAge(20); + _user.setSex("F"); + // 执行数据插入 + session.insert(_user); + + // 或者在插入时也可以指定/排除某些字段 + session.insert(_user, Fields.create(User.FIELDS.SEX, User.FIELDS.AGE).excluded(true)); + + + 更新(Update): + + User _user = new User(); + _user.setId("bc19f5645aa9438089c5e9954e5f1ac5"); + _user.setPwd(DigestUtils.md5Hex("654321")); + // 更新指定的字段 + session.update(_user, Fields.create(User.FIELDS.PWD)); + + + 查询(Find): + + - 方式一:通过数据实体设置条件(非空属性之间将使用and条件连接),查询所有符合条件的记录; + + User _user = new User(); + _user.setUsername("suninformation"); + _user.setPwd(DigestUtils.md5Hex("123456")); + // 返回所有字段 + IResultSet _users = session.find(_user); + // 或者返回指定的字段 + _users = session.find(_user, Fields.create(User.FIELDS.ID, User.FIELDS.AGE)); + + - 方式二:通过自定义条件,查询所有符合条件的记录; + + IResultSet _users = session.find( + EntitySQL.create(User.class) + .field(User.FIELDS.ID) + .field(User.FIELDS.SEX), + // 设置Order By条件 + Where.create() + .orderDesc(User.FIELDS.USER_NAME)); + + - 方式三:分页查询; + + IResultSet _users = session.find( + EntitySQL.create(User.class) + .field(User.FIELDS.ID) + .field(User.FIELDS.SEX), + Where.create() + .orderDesc(User.FIELDS.USER_NAME), + // 查询第1页,每页10条记录,统计总记录数 + Page.create(1).pageSize(10).count(true)); + + - 方式四:仅返回符合条件的第一条记录(FindFirst); + + // 查询用户名称和密码都匹配的第一条记录 + User _user = session.findFirst(EntitySQL.create(User.class), + Where.create( + Cond.create() + .eq(User.FIELDS.USER_NAME).param("suninformation") + .and() + .eq(User.FIELDS.PWD).param(DigestUtils.md5Hex("123456")))); + + **注**:更多的查询方式将在后面的 “查询(Query)” 章节中详细阐述; + + + 删除(Delete): + + - 根据实体主键删除记录: + + User _user = new User(); + _user.setId("bc19f5645aa9438089c5e9954e5f1ac5"); + // + session.delete(_user); + + // + session.delete(User.class, "bc19f5645aa9438089c5e9954e5f1ac5"); + + - 根据条件删除记录: + + // 删除年龄小于20岁的用户记录 + session.executeForUpdate( + SQL.create( + Delete.create(User.class).where( + Where.create( + Cond.create() + .lt(User.FIELDS.AGE).param(20))))); + + + 统计(Count): + + // 统计年龄小于20岁的用户记录总数 + + // 方式一: + long _count = session.count(User.class, + Where.create( + Cond.create() + .lt(User.FIELDS.AGE).param(20))); + + // 方式二: + _count = session.count( + SQL.create( + Delete.create(User.class).where( + Where.create( + Cond.create() + .lt(User.FIELDS.AGE).param(20))))); + + + + 执行更新类操作(ExecuteForUpdate): + + 该方法用于执行ISession接口中并未提供对应的方法封装且执行操作会对数据库产生变化的SQL语句,执行该方法后将返回受影响记录行数,如上面执行的删除年龄小于20岁的用户记录: + + int _effectCount =session.executeForUpdate( + SQL.create( + Delete.create(User.class).where( + Where.create( + Cond.create() + .lt(User.FIELDS.AGE).param(20))))); + + **注**:以上操作均支持批量操作,具体使用请阅读API接口文档和相关源码; + +## 数据实体操作 + +上面阐述的是基于ISession会话对象完成一系列数据库操作,接下来介绍的操作过程更加简单直接,完全基于数据实体对象; + +> 注意:本小节所指的数据实体对象必须通过继承框架提供BaseEntity抽象类; + +### 插入(Insert) + + User _user = new User(); + _user.setId(UUIDUtils.UUID()); + _user.setUsername("suninformation"); + _user.setPwd(DigestUtils.md5Hex("123456")); + _user.setAge(20); + _user.setSex("F"); + // 执行数据插入 + _user.save(); + + // 或者在插入时也可以指定/排除某些字段 + _user.save(Fields.create(User.FIELDS.SEX, User.FIELDS.AGE).excluded(true)); + + // 或者插入前判断记录是否已存在,若已存在则执行记录更新操作 + _user.saveOrUpdate(); + + // 或者执行记录更新操作时仅更新指定的字段 + _user.saveOrUpdate(Fields.create(User.FIELDS.SEX, User.FIELDS.AGE)); + +### 更新(Update) + + User _user = new User(); + _user.setId("bc19f5645aa9438089c5e9954e5f1ac5"); + _user.setPwd(DigestUtils.md5Hex("654321")); + _user.setAge(20); + _user.setSex("F"); + // 执行记录更新 + _user.update(); + + // 或者仅更新指定的字段 + _user.update(Fields.create(User.FIELDS.SEX, User.FIELDS.AGE)); + +### 查询(Find) + ++ 根据记录ID加载: + + User _user = new User(); + _user.setId("bc19f5645aa9438089c5e9954e5f1ac5"); + // 根据记录ID加载全部字段 + _user = _user.load(); + + // 或者根据记录ID加载指定的字段 + _user = _user.load(Fields.create(User.FIELDS.USER_NAME, User.FIELDS.SEX, User.FIELDS.AGE)); + ++ 通过数据实体设置条件(非空属性之间将使用and条件连接),查询所有符合条件的记录; + + User _user = new User(); + _user.setUsername("suninformation"); + _user.setPwd(DigestUtils.md5Hex("123456")); + // 返回所有字段 + IResultSet _users = _user.find(); + + // 或者返回指定的字段 + _users = _user.find(Fields.create(User.FIELDS.ID, User.FIELDS.AGE)); + + // 或者分页查询 + _users = _user.find(Page.create(1).pageSize(10)); + ++ 分页查询: + + User _user = new User(); + _user.setSex("F"); + + // 分页查询,返回全部字段 + IResultSet _users = _user.find(Page.create(1).pageSize(10)); + + // 或者分页查询,返回指定的字段 + _users = _user.find(Fields.create(User.FIELDS.ID, User.FIELDS.AGE), Page.create(1).pageSize(10)); + ++ 仅返回符合条件的第一条记录(FindFirst): + + User _user = new User(); + _user.setUsername("suninformation"); + _user.setPwd(DigestUtils.md5Hex("123456")); + + // 返回与用户名称和密码匹配的第一条记录 + _user = _user.findFirst(); + + // 或者返回与用户名称和密码匹配的第一条记录的ID和AGE字段 + _user = _user.findFirst(Fields.create(User.FIELDS.ID, User.FIELDS.AGE)); + +### 删除(Delete) + + User _user = new User(); + _user.setId("bc19f5645aa9438089c5e9954e5f1ac5"); + + // 根据实体主键删除记录 + _user.delete(); + +**注**:以上介绍的两种数据库操作方式各有特点,请根据实际情况选择更适合的方式,亦可混合使用; + + +## 结果集(ResultSet) + +JDBC模块将数据查询的结果集合统一使用IResultSet接口进行封装并集成分页参数,下面通过一段代码介绍如何使用IResultSet对象: + + IResultSet _results = JDBC.get().openSession(new ISessionExecutor>() { + public IResultSet execute(ISession session) throws Exception { + return session.find(EntitySQL.create(User.class), Page.create(1).pageSize(10)); + } + }); + + // 返回当前是否分页查询 + boolean _isPaginated = _results.isPaginated(); + + // 当前结果集是否可用,即是否为空或元素数量为0 + boolean _isAvailable = _results.isResultsAvailable(); + + // 返回当前页号 + int _pNumber = _results.getPageNumber(); + + // 返回每页记录数 + int _pSize = _results.getPageSize(); + + // 返回总页数 + int _pCount = _results.getPageCount(); + + // 返回总记录数 + long _rCount = _results.getRecordCount(); + + // 返回结果集数据 + List _users = _results.getResultData(); + +> **注意**: +> +> - Page分页参数将影响总页数和总记录数的返回值是否为0; +> +> > 当执行Page.create(1).pageSize(10).count(false)时,将不进行总记录数的count计算; +> +> - 非分页查询时返回的分页参数值均为0; + + +## 查询(Query) + +本节主要介绍YMP框架v2版本中新增的特性,辅助开发人员像写Java代码一样编写SQL语句,在一定程度上替代传统字符串拼接的模式,再配合数据实体的字段常量一起使用,这样做的好处就是降低字符串拼接过程中出错的机率,一些特定问题编译期间就能发现,因为Java代码就是SQL语句! + +### 基础参数对象 + +- Fields:字段名称集合对象,用于辅助拼接数据表字段名称,支持前缀、别名等; + + 示例代码: + + // 创建Fields对象 + Fields _fields = Fields.create("username", "pwd", "age"); + // 带前缀和别名 + _fields.add("u", "sex", "s"); + // 带前缀 + _fields = Fields.create().add("u", "id").add(_fields); + // 标记集合中的字段为排除的 + _fields.excluded(true); + // 判断是否存在排除标记 + _fields.isExcluded(); + // 输出 + System.out.println(_fields.fields()); + + 执行结果: + + [u.id, username, pwd, age, u.sex s] + + +- Params:参数集合对象,主要用于存储替换SQL语句中?号占位符; + + 示例代码: + + // 创建Params对象,任何类型参数 + Params _params = Params.create("p1", 2, false, 0.1).add("param"); + // + _params = Params.create().add("paramN").add(_params); + // 输出 + System.out.println(_params.params()); + + 执行结果: + + [paramN, p1, 2, false, 0.1, param] + +- Pages:分页参数对象; + + 示例代码: + + // 查询每1页, 默认每页20条记录 + Page.create(1); + // 查询第1页, 每页10条记录 + Page.create(1).pageSize(10); + // 查询第1页, 每页10条记录, 不统计总记录数 + Page.create(1).pageSize(10).count(false); + +- Cond:条件参数对象,用于生成SQL条件和存储条件参数; + + 示例代码: + + > 生成如下SQL条件: + > + > - (username like ? and age >= ?) or (sex = ? and age < ?) + + Cond _cond = Cond.create() + .bracketBegin().like("username").param("%ymp%").and().gtEq("age").param(20).bracketEnd() + .or() + .bracketBegin().eq("sex").param("F").and().lt("age").param(18).bracketEnd(); + + System.out.println("SQL: " + _cond.toString()); + System.out.println("参数: " + _cond.params().params()); + + 执行结果: + + SQL: ( username LIKE ? AND age >= ? ) OR ( sex = ? AND age < ? ) + 参数: [%ymp%, 20, F, 18] + +- OrderBy:排序对象,用于生成SQL条件中的Order By语句; + + 示例代码: + + OrderBy _orderBy = OrderBy.create().asc("age").desc("u", "birthday"); + // + System.out.println(_orderBy.toSQL()); + + 执行结果: + + ORDER BY age, u.birthday DESC + +- GroupBy:分组对象,用于生成SQL条件中的Group By语句; + + 示例代码: + + GroupBy _groupBy = GroupBy.create(Fields.create().add("u", "sex").add("dept")) + .having(Cond.create().lt("age").param(18)); + + System.out.println("SQL: " + _groupBy.toString()); + System.out.println("参数: " + _groupBy.having().params().params()); + + 执行结果: + + SQL: GROUP BY u.sex, dept HAVING age < ? + 参数: [18] + +- Where:Where语句对象,用于生成SQL语句中的Where子句; + + 示例代码: + + Cond _cond = Cond.create() + .like("username").param("%ymp%") + .and().gtEq("age").param(20); + + OrderBy _orderBy = OrderBy.create().asc("age").desc("u", "birthday"); + + GroupBy _groupBy = GroupBy.create(Fields.create().add("u", "sex").add("dept")); + + Where _where = Where.create(_cond).groupBy(_groupBy).orderDesc("username"); + + _where.orderBy().orderBy(_orderBy); + // + System.out.println("SQL: " + _where.toString()); + System.out.println("参数: " + _where.getParams().params()); + + 执行结果:(为方便阅读,此处美化了SQL的输出格式:P) + + SQL: WHERE + username LIKE ? + AND age >= ? + GROUP BY + u.sex, + dept + ORDER BY + username DESC, + age, + u.birthday DESC + 参数: [%ymp%, 20] + +- Join:连接语句对象,用于生成SQL语句中的Join子句,支持left、right和inner连接; + + 示例代码: + + Join _join = Join.inner("user_ext").alias("ue") + .on(Cond.create().opt("ue", "uid", Cond.OPT.EQ, "u", "id")); + + System.out.println(_join); + + 执行结果: + + INNER JOIN user_ext ue ON ue.uid = u.id + +- Union:联合语句对象,用于将多个Select查询结果合并; + + 示例代码: + + Select _select = Select.create("user").where(Where.create(Cond.create().eq("dept").param("IT"))) + .union(Union.create( + Select.create("user").where(Where.create(Cond.create().lt("age").param(18))))); + // + System.out.println("SQL: " + _select.toString()); + System.out.println("参数: " + _select.getParams().params()); + + 执行结果: + + SQL: SELECT * FROM user WHERE dept = ? UNION SELECT * FROM user WHERE age < ? + 参数: [IT, 18] + +### Select:查询语句对象 + + 示例代码: + + Cond _cond = Cond.create() + .like("u", "username").param("%ymp%") + .and().gtEq("u", "age").param(20); + // + GroupBy _groupBy = GroupBy.create(Fields.create().add("u", "sex").add("u", "dept")); + // + Where _where = Where.create(_cond).groupBy(_groupBy).orderDesc("u", "username"); + // + Join _join = Join.inner("user_ext").alias("ue") + .on(Cond.create().opt("ue", "uid", Cond.OPT.EQ, "u", "id")); + // + Select _select = Select.create(User.class, "u") + .field("u", "username").field("ue", "money") + .where(_where) + .join(_join) + .distinct(); + // + System.out.println("SQL: " + _select.toString()); + System.out.println("参数: " + _select.getParams().params()); + + 执行结果:(为方便阅读,此处美化了SQL的输出格式:P) + + SQL: SELECT DISTINCT + u.username, + ue.money + FROM + USER u + INNER JOIN user_ext ue ON ue.uid = u.id + WHERE + u.username LIKE ? + AND u.age >= ? + GROUP BY + u.sex, + u.dept + ORDER BY + u.username DESC + 参数: [%ymp%, 20] + +### Insert:插入语句对象 + + 示例代码: + + Insert _insert = Insert.create(User.class) + .field(User.FIELDS.ID).param("123456") + .field(User.FIELDS.AGE).param(18) + .field(User.FIELDS.USER_NAME).param("suninformation"); + // + System.out.println("SQL: " + _insert.toString()); + System.out.println("参数: " + _insert.params().params()); + + 执行结果: + + SQL: INSERT INTO user (id, age, username) VALUES (?, ?, ?) + 参数: [123456, 18, suninformation] + +### Update:更新语句对象 + + 示例代码: + + Update _update = Update.create(User.class) + .field(User.FIELDS.PWD).param("xxxx") + .field(User.FIELDS.AGE).param(20) + .where(Where.create( + Cond.create().eq(User.FIELDS.ID).param("123456"))); + // + System.out.println("SQL: " + _update.toString()); + System.out.println("参数: " + _update.getParams().params()); + + 执行结果: + + SQL: UPDATE user SET pwd = ?, age = ? WHERE id = ? + 参数: [xxxx, 20, 123456] + + +### Delete:删除语句对象 + + 示例代码: + + Delete _delete = Delete.create(User.class) + .where(Where.create( + Cond.create().eq(User.FIELDS.ID).param("123456"))); + // + System.out.println("SQL: " + _delete.toString()); + System.out.println("参数: " + _delete.getParams().params()); + + 执行结果: + + SQL: DELETE FROM user WHERE id = ? + 参数: [123456] + +### SQL:自定义SQL语句 + +同时也用于ISession会话接口参数封装; + +示例代码: + + // 自定义SQL语句 + SQL _sql = SQL.create("select * from user where age > ? and username like ?").param(18).param("%ymp%"); + // 执行 + session.find(_sql, IResultSetHandler.ARRAY); + + // 或封装语句对象 + SQL.create(_select); + SQL.create(_insert); + SQL.create(_update); + SQL.create(_delete); + +### BatchSQL:批量SQL语句对象 + +主要用于ISession会话对批量操作的参数封装; + +示例代码: + + // 定义批操作 + BatchSQL _sqls = BatchSQL.create("INSERT INTO user (id, age, username) VALUES (?, ?, ?)") + .addParameter(Params.create("xxxx", 18, "user0")) + .addParameter(Params.create("xxx1", 20, "user1")) + .addParameter(Params.create("xxxN", 20, "userN")) + .addSQL("DELETE FROM user WHERE age > 30") + .addSQL("DELETE FROM user WHERE age < 18"); + // 执行 + session.executeForUpdate(_sqls); + +### EntitySQL:实体参数封装对象 + +主要用于ISession会话的参数封装; + +示例代码: + + session.find(EntitySQL.create(User.class) + .field(Fields.create(User.FIELDS.ID, User.FIELDS.USER_NAME) + .excluded(true))); + +## 存储器(Repository) + +为了能够更方便的维护和执行SQL语句,JDBC模块提供了存储器的支持,可以通过`@Repository`注解自定义SQL或自定义SQL语句或从配置文件中加载SQL并自动执行。 + +- @Repository注解: + + 参数说明: + + > dsName:数据源名称,默认为空则采用默认数据源; + > + > item:从资源文件中加载item指定的配置项,默认为空; + > + > value:自定义SQL配置(value的优先级高于item); + > + > type:操作类型,默认为查询,可选值:Type.OPT.QUERY或Type.OPT.UPDATE + > + > useFilter:是否调用方法过滤, 默认为true + > + > dbType:指定当前存储器适用的数据库类型,默认为全部,否则将根据数据库类型进行存储器加载 + + +- 存储器示例代码: + + + 存储器: + + @Repository + public class DemoRepository implements IRepository { + + /** + * 注入SQL配置文件(任意配置对象均可) + */ + @Inject + private DefaultRepoConfig _repoCfg; + + /** + * 返回SQL配置文件对象, 若不需要配置文件请不要实现IRepository接口 + */ + public IConfiguration getConfig() { + return _repoCfg; + } + + /** + * 自定义SQL + */ + @Repository("select * from ymcms_attachment where hash = ${hash}") + public IResultSet getSQLResults(String hash, IResultSet results) throws Exception { + return results; + } + + /** + * 读取配置文件中的SQL + */ + @Repository(item = "demo_query") + public List getAttachments(String hash, IResultSet... results) throws Exception { + final List _returnValues = new ArrayList(); + if (results != null && results.length > 0) { + ResultSetHelper _help = ResultSetHelper.bind(results[0]); + if (_help != null) + _help.forEach(new ResultSetHelper.ItemHandler() { + @Override + public boolean handle(ResultSetHelper.ItemWrapper wrapper, int row) throws Exception { + _returnValues.add(wrapper.toEntity(new Attachment())); + return true; + } + }); + } + return _returnValues; + } + } + + + SQL配置文件对象: + + @Configuration("cfgs/default.repo.xml") + public class DefaultRepoConfig extends DefaultConfiguration { + } + + + SQL配置文件`default.repo.xml`内容: + + + + + + + + + + + + + + 在控制器中调用:在浏览器中访问`http://localhost:8080/hello`查看执行结果 + + @Controller + @RequestMapping("/hello") + public class HelloController { + + /** + * 注入存储器 + */ + @Inject + private DemoRepository _repo; + + @RequestMapping("/") + public IView hello() throws Exception { + // 调用存储器方法 + return View.jsonView(_repo.getAttachments("44f5b005c7a94a0d42f53946f16b6bb2")); + } + } + +- 说明: + + > - 存储器类通过声明`@Repository`注解被框架自动扫描并加载; + > - 与其它被容器管理的`@Bean`一样支持拦截器、事务、缓存等注解; + > - 当`useFilter=true`时,存储器类方法的参数至少有一个参数(方法有多个参数时,采用最后一个参数)用于接收SQL执行结果; + > - 查询类型SQL的执行结果数据类型为`IResultSet`,更新类型SQL的执行结果数据类型为`int`; + > - 用于接收SQL执行结果的方法参数支持变长类型,如:`IResultSet results`和`IResultSet... results`是一样的; + > - 读取配置文件中的SQL配置时,配置项名称必须全部采用小写字符,如:`demo_query`; + > - 框架将优先加载以当前数据源连接的数据库类型名称作为后缀的配置项,如:`demo_query_mysql`、`demo_query_oracle`,若找不到则加载默认名称,即`demo_query`; + +## 高级特性 + +### 多表查询及自定义结果集数据处理 + +JDBC模块提供的ORM主要是针对单实体操作,实际业务中往往会涉及到多表关联查询以及返回多个表字段,在单实体ORM中是无法将JDBC结果集记录自动转换为实体对象的,这时就需要对结果集数据自定义处理来满足业务需求。 + +若想实现结果集数据的自定义处理,需要了解以下相关接口和类: + ++ IResultSetHandler接口:结果集数据处理接口,用于完成将JDBC结果集原始数据的每一行记录进行转换为目标对象,JDBC模块默认提供了该接口的三种实现: + + > EntityResultSetHandler:采用实体类存储结果集数据的接口实现,此类已经集成在ISession会话接口业务逻辑中,仅用于处理单实体的数据转换; + > + > BeanResultSetHandler:将数据直接映射到类成员属性的结果集处理接口实现; + > + > MapResultSetHandler:采用Map存储结果集数据的接口实现; + > + > ArrayResultSetHandler:采用Object[]数组存储结果集数据的接口实现; + ++ ResultSetHelper类:数据结果集辅助处理工具,用于帮助开发人员便捷的读取和遍历结果集中数据内容,仅支持由 ArrayResultSetHandler 和 MapResultSetHandler 产生的结果集数据类型; + +下面通过简单的多表关联查询来介绍IResultSetHandler接口和ResultSetHelper类如何配合使用: + +示例代码一:使用ArrayResultSetHandler或MapResultSetHandler处理结果集数据; + + IResultSet _results = JDBC.get().openSession(new ISessionExecutor>() { + public IResultSet execute(ISession session) throws Exception { + // 通过查询对象创建SQL语句: + // + // SELECT u.id id, u.username username, ue.money money + // FROM user u LEFT JOIN user_ext ue ON u.id = ue.uid + // + Select _uSelect = Select.create(User.class, "u") + .join(Join.left(UserExt.TABLE_NAME).alias("ue") + .on(Cond.create() + .opt("u", User.FIELDS.ID, Cond.OPT.EQ, "ue", UserExt.FIELDS.UID))) + .field(Fields.create() + .add("u", User.FIELDS.ID, "id") + .add("u", User.FIELDS.USER_NAME, "username") + .add("ue", UserExt.FIELDS.MONEY, "money")); + + // 执行查询并指定采用Object[]数组存储结果集数据,若采用Map存储请使用:IResultSetHandler.MAP + return session.find(SQL.create(_uSelect), IResultSetHandler.ARRAY); + } + }); + + // 采用默认步长(step=1)逐行遍历 + ResultSetHelper.bind(_results).forEach(new ResultSetHelper.ItemHandler() { + public boolean handle(ResultSetHelper.ItemWrapper wrapper, int row) throws Exception { + System.out.println("当前记录行数: " + row); + + // 通过返回的结果集字段名取值 + String _id = wrapper.getAsString("id"); + String _uname = wrapper.getAsString("username"); + + // 也可以通过索引下标取值 + Double _money = wrapper.getAsDouble(2); + + // 也可以直接将当前行数据赋值给实体对象或自定义JavaBean对象 + wrapper.toEntity(new User()); + + // 当赋值给自定义的JavaBean对象时需要注意返回的字段名称与对象成员属性名称要一一对应并且要符合命名规范 + // 例如:对象成员名称为"userName",将与名称为"user_name"的字段对应 + wrapper.toObject(new User()); + + // 返回值将决定遍历是否继续执行 + return true; + } + }); + + // 采用指定的步长进行数据遍历,此处step=2 + ResultSetHelper.bind(_results).forEach(2, new ResultSetHelper.ItemHandler() { + public boolean handle(ResultSetHelper.ItemWrapper wrapper, int row) throws Exception { + // 代码略...... + return true; + } + }); + +示例代码二:使用自定义IResultSetHandler处理结果集数据; + + // 自定义JavaBean对象,用于封装多表关联的结果集的记录 + public class CustomUser { + + private String id; + + private String username; + + private Double money; + + // 忽略Getter和Setter方法 + } + + // 修改示例一的代码,将结果集中的每一条记录转换成自定义的CustomUser对象 + IResultSet _results = JDBC.get().openSession(new ISessionExecutor>() { + public IResultSet execute(ISession session) throws Exception { + Select _uSelect = Select.create(User.class, "u") + .join(Join.left(UserExt.TABLE_NAME).alias("ue") + .on(Cond.create() + .opt("u", User.FIELDS.ID, Cond.OPT.EQ, "ue", UserExt.FIELDS.UID))) + .field(Fields.create() + .add("u", User.FIELDS.ID, "id") + .add("u", User.FIELDS.USER_NAME, "username") + .add("ue", UserExt.FIELDS.MONEY, "money")); + + // 通过实现IResultSetHandler接口实现结果集的自定义处理 + return session.find(SQL.create(_uSelect), new IResultSetHandler() { + public List handle(ResultSet resultSet) throws Exception { + List _results = new ArrayList(); + while (resultSet.next()) { + CustomUser _cUser = new CustomUser(); + _cUser.setId(resultSet.getString("id")); + _cUser.setUsername(resultSet.getString("username")); + _cUser.setMoney(resultSet.getDouble("money")); + // + _results.add(_cUser); + } + return _results; + } + }); + } + }); + +### 存储过程调用与结果集数据处理 + +针对于存储过程,JDBC模块提供了`IProcedureOperator`操作器接口及其默认接口实现类`DefaultProcedureOperator`来帮助你完成,存储过程有以下几种调用方式,举例说明: + ++ 有输入参数无输出参数: + + IConnectionHolder _conn = JDBC.get().getDefaultConnectionHolder(); + try { + // 执行名称为`procedure_name`的存储过程,并向该存储过程转入两个字符串参数 + IProcedureOperator _opt = new DefaultProcedureOperator("procedure_name", _conn) + .addParameter("param1") + .addParameter("param2") + .execute(IResultSetHandler.ARRAY); + // 遍历结果集集合 + for (List _item : _opt.getResultSets()) { + ResultSetHelper.bind(_item).forEach(new ResultSetHelper.ItemHandler() { + public boolean handle(ResultSetHelper.ItemWrapper wrapper, int row) throws Exception { + System.out.println(wrapper.toObject(new ArchiveVObject()).toJSON()); + return true; + } + }); + } + } finally { + _conn.release(); + } + ++ 有输入输出参数: + + IConnectionHolder _conn = JDBC.get().getDefaultConnectionHolder(); + try { + // 通过addOutParameter方法按存储过程输出参数顺序指定JDBC参数类型 + new DefaultProcedureOperator("procedure_name", _conn) + .addParameter("param1") + .addParameter("param2") + .addOutParameter(Types.VARCHAR) + .execute(new IProcedureOperator.IOutResultProcessor() { + public void process(int idx, int paramType, Object result) throws Exception { + System.out.println(result); + } + }); + } finally { + _conn.release(); + } + ++ 另一种写法: + + JDBC.get().openSession(new ISessionExecutor>>() { + public List> execute(ISession session) throws Exception { + // 创建存储过程操作器对象 + IProcedureOperator _opt = new DefaultProcedureOperator("procedure_name", session.getConnectionHolder()) + .addParameter("param1") + .addParameter("param2") + .addOutParameter(Types.VARCHAR) + .addOutParameter(Types.INTEGER) + .setOutResultProcessor(new IProcedureOperator.IOutResultProcessor() { + public void process(int idx, int paramType, Object result) throws Exception { + System.out.println(result); + } + }).setResultSetHandler(IResultSetHandler.ARRAY); + // 执行 + _opt.execute(); + return _opt.getResultSets(); + } + }); + +### 数据库锁操作 + +数据库是一个多用户使用的共享资源,当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况,若对并发操作不加以控制就可能会造成数据的错误读取和存储,破坏数据库的数据一致性,所以说,加锁是实现数据库并发控制的一个非常重要的技术; + +> 数据库加锁的流程是:当事务在对某个数据对象进行操作前,先向系统发出请求对其加锁,加锁后的事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作; + +因此,JDBC模块在数据库查询操作中集成了针对数据库记录锁的控制能力,称之为IDBLocker,以参数的方式使用起来同样的简单! + +首先了解一下IDBLocker提供的锁的类型: + ++ MySQL: + + > IDBLocker.DEFAULT:行级锁,只有符合条件的数据被加锁,其它进程等待资源解锁后再进行操作; + ++ Oracle: + + > IDBLocker.DEFAULT:行级锁,只有符合条件的数据被加锁,其它进程等待资源解锁后再进行操作; + > + > IDBLocker.ORACLE_NOWAIT:行级锁,不进行资源等待,只要发现结果集中有些数据被加锁,立刻返回“ORA-00054错误”; + ++ SQL Server: + + > IDBLocker.SQLSERVER_NOLOCK:不加锁,在读取或修改数据时不加任何锁; + > + > IDBLocker.SQLSERVER_HOLDLOCK:保持锁,将此共享锁保持至整个事务结束,而不会在途中释放; + > + > IDBLocker.SQLSERVER_UPDLOCK:修改锁,能够保证多个进程能同时读取数据但只有该进程能修改数据; + > + > IDBLocker.SQLSERVER_TABLOCK:表锁,整个表设置共享锁直至该命令结束,保证其他进程只能读取而不能修改数据; + > + > IDBLocker.SQLSERVER_PAGLOCK:页锁; + > + > IDBLocker.SQLSERVER_TABLOCKX:排它表锁,将在整个表设置排它锁,能够防止其他进程读取或修改表中的数据; + ++ 其它数据库: + + > 可以通过IDBLocker接口自行实现; + +下面通过示例代码展示如何使用锁: + +示例代码一:通过EntitySQL对象传递锁参数; + + session.find(EntitySQL.create(User.class) + .field(Fields.create(User.FIELDS.ID, User.FIELDS.USER_NAME).excluded(true)) + .forUpdate(IDBLocker.DEFAULT)); + +示例代码二:通过Select查询对象传递锁参数; + + Select _select = Select.create(User.class, "u") + .field("u", "username").field("ue", "money") + .where(Where.create( + Cond.create().eq(User.FIELDS.ID).param("bc19f5645aa9438089c5e9954e5f1ac5"))) + .forUpdate(IDBLocker.DEFAULT); + + session.find(SQL.create(_select), IResultSetHandler.ARRAY); + +示例代码三:基于数据实体对象传递锁参数 + + // + User _user = new User(); + _user.setId("bc19f5645aa9438089c5e9954e5f1ac5"); + // + _user.load(IDBLocker.DEFAULT); + + // + User _user = new User(); + _user.setUsername("suninformation"); + _user.setPwd(DigestUtils.md5Hex("123456")); + // + IResultSet _users = _user.find(IDBLocker.DEFAULT); + +> **注意**: +> +> 请谨慎使用数据库锁机制,尽量避免产生锁表,以免发生死锁情况! \ No newline at end of file diff --git a/misc/site/guide/persistence/mongodb.md b/misc/site/guide/persistence/mongodb.md new file mode 100755 index 00000000..e87bf4bc --- /dev/null +++ b/misc/site/guide/persistence/mongodb.md @@ -0,0 +1,5 @@ +--- +sidebarDepth: 2 +--- + +# MongoDB \ No newline at end of file diff --git a/misc/site/guide/persistence/redis.md b/misc/site/guide/persistence/redis.md new file mode 100755 index 00000000..423883e4 --- /dev/null +++ b/misc/site/guide/persistence/redis.md @@ -0,0 +1,223 @@ +--- +sidebarDepth: 2 +--- + +# Redis + +基于Jedis驱动封装,以JDBC模块的设计思想进行简单封装,采用会话机制,简化订阅(`subscribe`)和发布(`publish`)处理,支持多数据源及连接池配置,支持`jedis`、`shard`、`sentinel`和`cluster`等数据源连接方式; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-persistence-redis + + + +## 模块初始化配置 + + #------------------------------------- + # Redis持久化模块初始化参数 + #------------------------------------- + + # 默认数据源名称,默认值为default + ymp.configs.persistence.redis.ds_default_name= + + # 数据源列表,多个数据源名称间用'|'分隔,默认为default + ymp.configs.persistence.redis.ds_name_list= + + # 数据源连接方式, 默认为default,目前支持[default|shard|sentinel|cluster] + ymp.configs.persistence.redis.ds.default.connection_type= + + # Redis服务端名称列表, 多个服务端名称间用'|'分隔, 默认为default + ymp.configs.persistence.redis.ds.default.server_name_list= + + # 当connection_type=sentinel时, 参数master_server_name必须提供, 默认为default + ymp.configs.persistence.redis.ds.default.master_server_name= + + # 服务端--主机地址, 默认为localhost + ymp.configs.persistence.redis.ds.default.server.default.host= + + # 服务端--主机端口, 默认为6379 + ymp.configs.persistence.redis.ds.default.server.default.port= + + # 服务端--连接超时时间(毫秒), 默认为2000 + ymp.configs.persistence.redis.ds.default.server.default.timeout= + + # 服务端--超时时间(毫秒), 默认为2000 + ymp.configs.persistence.redis.ds.default.server.default.socket_timeout= + + # 服务端--最大尝试次数, 默认为3 + ymp.configs.persistence.redis.ds.default.server.default.max_attempts= + + # 服务端--连接权重, 默认为1 + ymp.configs.persistence.redis.ds.default.server.default.weight= + + # 服务端--数据库索引, 默认为0 + ymp.configs.persistence.redis.ds.default.server.default.database= + + # 服务端--客户端名称, 默认为空 + ymp.configs.persistence.redis.ds.default.server.default.client_name= + + # 服务端--身份认证密码, 选填, 默认为空 + ymp.configs.persistence.redis.ds.default.server.default.password= + + # 服务端--身份认证密码是否已加密,默认为false + ymp.configs.persistence.redis.ds.default.server.default.password_encrypted= + + # 服务端--密码处理器,可选参数,用于对已加密访问密码进行解密,默认为空 + ymp.configs.persistence.redis.ds.default.server.default.password_class= + + #------------------------------------- + # 连接池相关配置参数 + #------------------------------------- + + # 连接池--最大空闲连接数, 默认为8 + ymp.configs.persistence.redis.ds.default.pool.max_idle= + + # 连接池--最大连接数, 默认为8 + ymp.configs.persistence.redis.ds.default.pool.max_total= + + # 连接池--最小空闲连接数, 默认为0 + ymp.configs.persistence.redis.ds.default.pool.min_idle= + + # 连接池--连接耗尽时是否阻塞, false报异常, ture阻塞直到超时, 默认为true + ymp.configs.persistence.redis.ds.default.pool.block_when_exhausted= + + # 连接池-- + ymp.configs.persistence.redis.ds.default.pool.fairness=false + + # 连接池--设置逐出策略类名, 默认为DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数) + ymp.configs.persistence.redis.ds.default.pool.eviction_policy_class_name= + + # 连接池--是否启用JMX管理功能, 默认为true + ymp.configs.persistence.redis.ds.default.pool.jmx_enabled= + + # 连接池-- + ymp.configs.persistence.redis.ds.default.pool.jmx_name_base=pool + + # 连接池-- + ymp.configs.persistence.redis.ds.default.pool.jmx_name_prefix=pool + + # 连接池--是否启用后进先出, 默认为true + ymp.configs.persistence.redis.ds.default.pool.lifo=true + + # 连接池--获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted), 如果超时就抛异常, 小于零:阻塞不确定的时间, 默认为-1 + ymp.configs.persistence.redis.ds.default.pool.max_wait_millis=-1 + + # 连接池-- + ymp.configs.persistence.redis.ds.default.pool.min_evictable_idle_time_millis= + + # 连接池--对象空闲多久后逐出, 当空闲时间>该值且空闲连接>最大空闲数时直接逐出, 不再根据MinEvictableIdleTimeMillis判断(默认逐出策略) + ymp.configs.persistence.redis.ds.default.pool.soft_min_evictable_idle_time_millis= + + # 连接池--在获取连接的时候检查有效性, 默认为false + ymp.configs.persistence.redis.ds.default.pool.test_on_borrow= + + # 连接池--, 默认为false + ymp.configs.persistence.redis.ds.default.pool.test_on_create= + + # 连接池--在归还到池中前进行检验, 默认为false + ymp.configs.persistence.redis.ds.default.pool.test_on_return= + + # 连接池--在空闲时检查有效性, 默认为false + ymp.configs.persistence.redis.ds.default.pool.test_while_idle= + + # 连接池--每次逐出检查时逐出的最大数目, 如果为负数就是1/abs(n), 默认为3 + ymp.configs.persistence.redis.ds.default.pool.num_tests_per_eviction_run= + + # 连接池--逐出扫描的时间间隔(毫秒), 如果为负数则不运行逐出线程, 默认为-1 + ymp.configs.persistence.redis.ds.default.pool.time_between_eviction_runs_millis= + +## 多数据源连接 + +Redis持久化模块默认支持多数据源配置,下面通过简单的配置来展示如何连接多个服务: + + # 定义两个数据源分别用于连接本地和另一台IP地址两个Redis服务 + ymp.configs.persistence.redis.ds_default_name=default + ymp.configs.persistence.redis.ds_name_list=default|otherredis + + # 默认数据源连接本地默认端口Redis服务 + ymp.configs.persistence.redis.ds.default.connection_type=default + ymp.configs.persistence.redis.ds.default.server.default.password=123456 + + # 名称otherredis数据源连接指定IP地址和端口的Redis服务 + ymp.configs.persistence.redis.ds.otherredis.connection_type=default + ymp.configs.persistence.redis.ds.otherredis.server.default.host=192.168.10.110 + ymp.configs.persistence.redis.ds.otherredis.server.default.port=86379 + ymp.configs.persistence.redis.ds.otherredis.server.default.database=1 + ymp.configs.persistence.redis.ds.otherredis.server.default.password=654321 + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置Redis模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + RedisModuleConfigurable.create().addDataSource( + RedisDataSourceConfigurable.create("default").addServer( + // 添加Redis服务器节点 + RedisServerConfigurable.create("default").host("localhost").port(6379)))).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Redis.class); + // 执行框架初始化 + owner.init(); + +## 示例代码 + +- 示例一:开启会话并手动关闭 + + // 方式一:开启默认Redis服务连接会话 + IRedisSession _session = Redis.get().openSession(); + // 方式二:开启指定数据源连接会话 + _session = Redis.get().openSession("otherredis"); + try { + // 通过会话接口可以获取Redis命令持有者接口对象实例 + IRedisCommandsHolder _holder = _session.getCommandHolder(); + // 可以通过命令持有者对象获取Jdeis对象和JedisCommands对象 + Jedis _jedis = _holder.getJedis(); + JedisCommands _commands = _holder.getCommands(); + // 示范写入key和value值 + _commands.set("key", "value"); + } finally { + // 一定要确保连接使用完毕后关闭会话以释放连接 + _session.close(); + } + +- 示例二:开启会话并在使用后自动关闭 + + // 方式一:开启默认数据源连接会话 + Redis.get().openSession(new IRedisSessionExecutor() { + @Override + public Object execute(IRedisSession session) throws Exception { + return session.getCommandHolder().getCommands().set("key", "value"); + } + }); + + // 方式二:开启指定数据源连接会话 + String _value = Redis.get().openSession("otherredis", new IRedisSessionExecutor() { + @Override + public String execute(IRedisSession session) throws Exception { + return session.getCommandHolder().getCommands().get("key"); + } + }); + +- 示例三:消息订阅 + + // 订阅缓存key过期通知 + Redis.get().subscribe(new JedisPubSub() { + @Override + public void onMessage(String channel, String message) { + System.out.println("channel: " + channel + ", message: " + message); + } + }, "__keyevent@0__:expired"); + + // 订阅指定数据源...通知 + Redis.get().subscribe("otherredis", new JedisPubSub() { + .... + }, "__keyevent@0__:expired"); + + **说明:** 订阅对象`JedisPubSub`将在服务停止时被自动取消订阅。 \ No newline at end of file diff --git a/misc/site/guide/plugin.md b/misc/site/guide/plugin.md new file mode 100755 index 00000000..8e41e8d6 --- /dev/null +++ b/misc/site/guide/plugin.md @@ -0,0 +1,324 @@ +--- +sidebarDepth: 2 +--- + +# 插件(Plugin) + +插件模块采用独立的ClassLoader类加载器来管理私有JAR包、类、资源文件等,设计目标是在接口开发模式下,将需求进行更细颗粒度拆分,从而达到一个理想化可重用代码的封装形态; + +每个插件都是封闭的世界,插件与外界之间沟通的唯一方法是通过业务接口调用,管理这些插件的容器被称之为插件工厂(IPluginFactory),负责插件的分析、加载和初始化,以及插件的生命周期管理,插件模块支持创建多个插件工厂实例,工厂对象之间完全独立,无任何依赖关系; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-plugin + + + +> **注**:在项目的pom.xml中添加上述配置,该模块已经默认引入核心包依赖,无需重复配置。 + +## 插件工厂 + +插件工厂分为两种,一种是以模块的形式封装,由YMP框架初始化时根据配置参数自动构建,称之为默认插件工厂(有且仅能存在一个默认工厂实例),另一种是通过代码手动配置构建的自定义插件工厂,不同之处在于默认插件工厂与框架结合得更紧密,两种模式可以并存; + +### 默认插件工厂 + +默认插件工厂是在插件模块被YMP框架初始化时自动创建的,其初始化参数及说明如下: + + #------------------------------------- + # Plugin插件模块初始化参数 + #------------------------------------- + + # 插件模块是否已被禁用(禁用后模块初始化时不执行初始化默认插件工厂和自动扫描), 默认值: false + ymp.configs.plugin.disabled= + + # 插件主目录路径,可选参数,默认值为${root}/plugins + ymp.configs.plugin.plugin_home= + + # 自动扫描包路径集合,多个包名之间用'|'分隔,默认与框架自动扫描的包路径相同 + ymp.configs.plugin.autoscan_packages= + + # 插件是否自动启动,默认为true + ymp.configs.plugin.automatic= + + # 是否加载当前CLASSPATH内的所有包含插件配置文件的JAR包,默认为true + ymp.configs.plugin.included_classpath= + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置插件模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + PluginModuleConfigurable.create() + .pluginHome("${root}/plugins") + .autoscanPackages("net.ymate") + .automatic(true) + .includedClasspath(true)).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Plugins.class); + // 执行框架初始化 + owner.init(); + +通过默认插件工厂获取插件的方法: + + Plugins.get().getPlugin(IDemoPlugin.class); + +默认插件工厂的事件监听方法: + +> 默认插件工厂是通过YMP框架的事件服务订阅进行处理,PluginEvent插件事件对象包括以下事件类型: + +|事务类型|说明| +|---|---| +|PLUGIN_INITED|插件初始化事件| +|PLUGIN_STARTED|插件启动事件| +|PLUGIN_SHUTDOWN|插件停止事件| +|PLUGIN_DESTROYED|插件销毁事件| + +## 自定义插件工厂 + +自定义插件工厂有两种方式: + +- 通过`@PluginFactory`注解配置插件工厂,注解参数说明如下: + + |参数|说明| + |---|---| + |pluginHome|插件存放路径,必需提供;| + |autoscanPackages|自动扫描路径,默认为插件工厂所在包路径;| + |automatic|插件是否自动启动,默认为true;| + |listenerClass|插件生命周期事件监听器类对象, 可选配置;| + + 示例代码: + + @PluginFactory(pluginHome = "${root}/plugins") + public class DemoPluginFactory extends DefaultPluginFactory { + } + + // 或者 + + @PluginFactory(pluginHome = "${root}/plugins", + autoscanPackages = {"com.company", "cn.company"}, + automatic = true, + includedClassPath = false, + listenerClass = DemoPluginEventListener.class) + public class DemoPluginFactory extends DefaultPluginFactory { + } + +- 通过工厂配置对象实例化 + + 创建工厂配置对象: + + IPluginConfig _conf = DefaultPluginConfig.create() + .pluginHome(new File(RuntimeUtils.replaceEnvVariable("${root}/plugins"))) + .automatic(true) + .autoscanPackages(Arrays.asList("com.company", "cn.company")) + .eventListener(new DefaultPluginEventListener()); + + 创建并初始化插件工厂实例对象: + + IPluginFactory _factory = new DefaultPluginFactory(YMP.get()); + _factory.init(_conf); + + 自定义插件工厂的事件监听方法: + + > 自定义插件工厂的事件处理方式与默认插件工厂不同,须通过实现IPluginEventListener接口完成插件生命周期事件监听,IPluginEventListener接口事件方法及说明如下: + + |事件|说明| + |---|---| + |onInited|插件初始化事件;| + |onStarted|插件启动事件;| + |onShutdown|插件停止事件;| + |onDestroy|插件销毁事件;| + + 示例代码: + + public class DemoPluginEventListener implements IPluginEventListener { + + public void onInited(IPluginContext context, IPlugin plugin) { + System.out.println("onInited: " + context.getPluginMeta().getName()); + } + + public void onStarted(IPluginContext context, IPlugin plugin) { + System.out.println("onStarted: " + context.getPluginMeta().getName()); + } + + public void onShutdown(IPluginContext context, IPlugin plugin) { + System.out.println("onShutdown: " + context.getPluginMeta().getName()); + } + + public void onDestroy(IPluginContext context, IPlugin plugin) { + System.out.println("onDestroy: " + context.getPluginMeta().getName()); + } + } + +## 插件结构 + +插件有两种形式,一种是将插件以JAR包文件形式存储,这类插件可以直接与工程类路径下其它依赖包一起使用,另一种是将插件类文件及插件依赖包等资源放在插件目录结构下,这类插件可以放在工程路径以外,可以多模块共用插件,其目录结构如下: + + \ + |--.plugin\ + | |--lib\ + | | |--xxxx.jar + | | |--... + | |--classes\ + | | |--... + | |--... + |--\ + | |--lib\ + | | |--xxxx.jar + | | |--... + | |--classes\ + | | |--... + | |--... + |--\ + |--... + +> 插件目录结构说明: + +> - 每一个插件工厂所指定的PLUGIN_HOME根路径下都可以通过一个名称为".plugin"的目录将一些JAR包或类等资源文件进行全局共享; + +> - 每一个插件都是一个独立的目录,一般以插件ID命名(不限于),并将插件相关的JAR包和类文件等资源分别放置在对应的lib、classes或其它目录下; + +## 插件 + +通过在一个实现了IPlugin接口的类上声明`@Plugin`注解来创建插件启动类,其将被插件工厂加载和管理,一个插件包可以包括多个插件启动类,每个插件启动类可以实现自己的业务接口对外提供服务; + +- `@Plugin`注解参数说明: + + > id:插件唯一ID,若未填写则使用初始化类名称进行MD5加密后的值做为ID; + > + > name:插件名称,默认为""; + > + > alias:插件别名,默认为""; + > + > author:插件作者,默认为""; + > + > email:联系邮箱,默认为""; + > + > version:插件版本,默认为"1.0.0"; + > + > automatic:是否加载后自动启动运行,默认true; + > + > description:插件描述,默认为""; + +- IPlugin接口方法说明: + + > init:插件初始化; + > + > getPluginContext:返回插件环境上下文对象; + > + > isInited:返回插件是否已初始化; + > + > isStarted:返回插件是否已启动; + > + > startup:启动插件; + > + > shutdown:停止插件; + > + > destroy:销毁插件对象; + +插件框架提供了一个封装了IPlugin接口的AbstractPlugin抽象类,建议直接继承,示例代码: + + @Plugin + public class DemoPlugin extends AbstractPlugin { + // 根据需要重写父类方法... + } + +结合业务接口的插件示例: + + // 定义一个业务接口 + public interface IBusiness { + void sayHi(); + } + + @Plugin(id = "demo_plugin", + name = "DemoPlugin", + author = "有理想的鱼", + email = "suninformaiton#163.com", + version = "1.0") + public class DemoPlugin extends AbstractPlugin implements IBusiness { + + @Override + public void startup() throws Exception { + super.startup(); + // + System.out.println("started."); + } + + @Override + public void shutdown() throws Exception { + super.shutdown(); + // + System.out.println("shutdown."); + } + + @Override + public void sayHi() { + System.out.println("Hi, from Plugin."); + } + } + +## 插件的使用 + +上面我们已经创建了一个DemoPlugin插件并且实现了IBusiness业务接口,下面介绍如何使用插件和调用业务接口方法: + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + DemoPlugin _plugin = (DemoPlugin) Plugins.get().getPlugin("demo_plugin"); + // 或者 + // _plugin = Plugins.get().getPlugin(DemoPlugin.class); + // + _plugin.sayHi(); + // + IBusiness _business = Plugins.get().getPlugin(IBusiness.class); + _business.sayHi(); + } finally { + YMP.get().destroy(); + } + } + +执行结果: + + Hi, from Plugin. + Hi, from Plugin. + shutdown. + +## 通过依赖注入引用插件 + +通过`@PluginRefer`注解指定注入插件实例,注解参数说明如下: + +|参数|说明| +|---|---| +|value|插件唯一ID,若不指定则默认根据成员对象类型查找插件| + +示例代码: + + @Bean + public class App { + + @PluginRefer + private IBusiness _business; + + public IBusiness getBusiness() { + return _business; + } + + public static void main(String[] args) throws Exception { + try { + YMP.get().init(); + // + App _app = YMP.get().getBean(App.class); + IBusiness _biz = _app.getBusiness(); + _biz.sayHi(); + } finally { + YMP.get().destroy(); + } + } + } + +**注**:同一个插件可以实现多个业务接口,若多个插件实现同一个业务接口,根据插件加载顺序,最后加载的插件实例对象将替换前者; \ No newline at end of file diff --git a/misc/site/guide/serv.md b/misc/site/guide/serv.md new file mode 100755 index 00000000..e122b20d --- /dev/null +++ b/misc/site/guide/serv.md @@ -0,0 +1,562 @@ +--- +sidebarDepth: 2 +--- + +# 服务(Serv) + +服务模块(Serv)是一套基于NIO实现的通讯服务框架,提供TCP、UDP协议的客户端与服务端封装,灵活的消息监听与消息内容编/解码,简约的配置使二次开发更加便捷; +同时默认提供断线重连、链路维护(心跳)等服务支持,您只需了解业务即可轻松完成开发工作。 + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-serv + + + +> **注**:在项目的pom.xml中添加上述配置,该模块已经默认引入核心包依赖,无需重复配置。 + + +## 基础概念 + +### 会话(Session) + +> 用于客户端与服务端之间连接状态的维护和消息发送的对象; + +### 编/解码器(Codec) + +> 目前提供以下两种编/解码器,开发者可通过实现ICodec接口自行扩展; + +> - NioStringCodec:采用字节`byte[4]`作为消息头,用于记录消息体长度的字符串消息编/解码器; + +> - TextLineCodec:用于解析以回车换行符(`\r\n`)做为消息结束标志的字符串消息的编/解码器; + +### 内置服务(Service) + +> 目前提供以下两种内置服务,更多服务在不断完善中...; + +> - IHeartbeatService:内置链路维护(心跳)服务,该服务将在与服务端成功建立连接后按参数配置的时间间隔向服务端发送心跳消息(心跳消息内容默认为0字符,心跳消息内容可以通过自定义参数heartbeat_message设置); + +> - IReconnectService:内置断线重连服务,当服务的连接状态异常时将尝试重新与服务端建立连接; + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置服务模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + ServModuleConfigurable.create() + // 添加服务端配置 + .addServer(ServServerConfigurable.create("default").serverHost("localhost").port(8281)) + // 添加客户端配置 + .addClient(ServClientConfigurable.create("default") + .remoteHost("localhost") + .port(8281) + .connectionTimeout(60) + .heartbeatInterval(30).reconnectionInterval(1))).build()) + .proxyFactory(new DefaultProxyFactory()) + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(Servs.class); + // 执行框架初始化 + owner.init(); + +## 服务端(Server) + +服务端初始化参数: + + #------------------------------------- + # 服务模块--服务端初始化参数 + #------------------------------------- + + # 服务端配置列表,多个服务端名称间用'|'分隔,默认为default + ymp.configs.serv.server.name_list=default + + # 绑定IP地址, 默认为0.0.0.0 + ymp.configs.serv.server.default.host=0.0.0.0 + + # 监听端口号, 默认为8281 + ymp.configs.serv.server.default.port=8281 + + # 编解码字符集, 默认为UTF-8 + ymp.configs.serv.server.default.charset=UTF-8 + + # 缓冲区大小, 默认为4096 + ymp.configs.serv.server.default.buffer_size=4096 + + # NIO选择器数量, 默认为1 + ymp.configs.serv.server.default.selector_count=1 + + # 执行线程池大小, 默认为 Runtime.getRuntime().availableProcessors() + ymp.configs.serv.server.default.executor_count=10 + + # 空闲线程等待新任务的最长时间, 默认为 0 + ymp.configs.serv.server.default.keep_alive_time=0 + + # 最大线程池大小,默认为 200 + ymp.configs.serv.server.default.thread_max_pool_size=200 + + # 线程队列大小,默认为 1024 + ymp.configs.serv.server.default.thread_queue_size=1024 + + # 自定义参数, 可选 + ymp.configs.serv.server.default.params.xxx=xxx + +通过在监听器实现类声明`@Server`注解来表示一个服务端,该注解有如下参数: + +|参数|说明| +|---|---| +|name|设置服务的名称,Serv框架将会根据该参数指定的名称加载对应的服务端参数配置,默认为default;| +|codec|设置编解码器,默认为NioStringCodec;| +|implClass|服务端实现类,默认为NioServer;| + +基于TCP协议的服务端,需要继承NioServerListener监听器类,支持监听如下事件: + +|事件|说明| +|---|---| +|onSessionAccepted|客户端成功接入服务端后触发该事件;| +|onBeforeSessionClosed|客户端会话被关闭之前触发该事件;| +|onAfterSessionClosed|客户端会话被关闭之后触发该事件;| +|onMessageReceived|收到客户端发送的消息时触发该事件;| +|onExceptionCaught|出现异常时触发该事件;| + +基于UDP协议的服务端,需要继承NioUdpListener监听器类,支持监听如下事件: + +|事件|说明| +|---|---| +|onSessionReady|客户端与服务端连接已建立并准备就绪时触发该事件;| +|onMessageReceived|收到客户端发送的消息时触发该事件;| +|onExceptionCaught|出现异常时触发该事件;| + +### 示例代码 + +#### TCP服务端 + + // 采用默认配置的TCP服务端 + @Server + public class TcpServer extends NioServerListener { + @Override + public void onSessionAccepted(INioSession session) throws IOException { + super.onSessionAccepted(session); + } + + @Override + public void onMessageReceived(Object message, INioSession session) throws IOException { + super.onMessageReceived(message, session); + // 输出接收到的消息 + System.out.println("Message received: " + message); + // 向客户端发送消息 + session.send("Hi, guys!"); + } + + @Override + public void onAfterSessionClosed(INioSession session) throws IOException { + super.onAfterSessionClosed(session); + } + + @Override + public void onBeforeSessionClosed(INioSession session) throws IOException { + super.onBeforeSessionClosed(session); + } + } + +#### UDP服务端 + + // 采用默认配置的UDP服务端,其中implClass参数必须指定为NioUpdServer.class + @Server(implClass = NioUdpServer.class, codec = TextLineCodec.class) + public class UdpServer extends NioUdpListener { + + public Object onSessionReady() throws IOException { + // 此接口方法的返回值将作为消息发送至客户端 + return null; + } + + public Object onMessageReceived(InetSocketAddress sourceAddr, Object message) throws IOException { + // 输出接收到的消息 + System.out.println("Message received: " + message); + // 此接口方法的返回值将作为消息发送至客户端 + return message; + } + + public void onExceptionCaught(InetSocketAddress sourceAddr, Throwable e) throws IOException { + System.out.println(sourceAddr + "--->" + e); + } + } + +## 客户端(Client): + +客户端初始化参数: + + #------------------------------------- + # 服务模块--客户端初始化参数 + #------------------------------------- + + # 客户端配置列表,多个客户端名称间用'|'分隔,默认为default + ymp.configs.serv.client.name_list=default + + # 远程主机IP地址, 默认为0.0.0.0 + ymp.configs.serv.client.default.host=0.0.0.0 + + # 远程主机端口号, 默认为8281 + ymp.configs.serv.client.default.port=8281 + + # 编解码字符集, 默认为UTF-8 + ymp.configs.serv.client.default.charset=UTF-8 + + # 缓冲区大小, 默认为4096 + ymp.configs.serv.client.default.buffer_size=4096 + + # 执行线程池大小, 默认为 Runtime.getRuntime().availableProcessors() + ymp.configs.serv.client.default.executor_count=10 + + # 连接超时时间(秒), 默认为30 + ymp.configs.serv.client.default.connection_timeout=30 + + # 断线重连检测间隔(秒), 默认为1 + ymp.configs.serv.client.default.reconnection_interval=1 + + # 心跳发送时间间隔(秒), 默认为60 + ymp.configs.serv.client.default.heartbeat_interval=60 + + # 自定义参数, 可选 + ymp.configs.serv.client.default.params.xxx=xxx + +通过在监听器实现类声明@Client注解来表示一个客户端,该注解有如下参数: + +|事件|说明| +|---|---| +|name|设置客户端名称,Serv框架将会根据该参数指定的名称加载对应的客户端参数配置,默认为default;| +|codec|设置编解码器,默认为NioStringCodec;| +|implClass|客户端实现类,默认为NioClient;| +|reconnectClass|短线重连服务实现类,默认为NONE;| +|heartbeatClass|链路维护(心跳)服务实现类,默认为NONE;| + +基于TCP协议的客户端,需要继承NioClientListener监听器类,支持监听如下事件: + +|事件|说明| +|---|---| +|onSessionConnected|客户端成功接入服务端后触发该事件;| +|onBeforeSessionClosed|客户端会话被关闭之前触发该事件;| +|onAfterSessionClosed|客户端会话被关闭之后触发该事件;| +|onMessageReceived|收到服务端发送的消息时触发该事件;| +|onExceptionCaught|出现异常时触发该事件;| + +基于UDP协议的客户端,需要继承NioUdpListener监听器类,支持监听如下事件: + +|事件|说明| +|---|---| +|onSessionReady|客户端与服务端连接已建立并准备就绪时触发该事件;| +|onMessageReceived|收到服务端发送的消息时触发该事件;| +|onExceptionCaught|出现异常时触发该事件;| + +### 示例代码 + +#### TCP客户端 + + @Client(reconnectClass = DefaultReconnectService.class, + heartbeatClass = DefaultHeartbeatService.class, codec = TextLineCodec.class) + public class TcpClient extends NioClientListener { + + @Override + public void onSessionConnected(INioSession session) throws IOException { + super.onSessionConnected(session); + // + session.send("Hello from client."); + } + + @Override + public void onMessageReceived(Object message, INioSession session) throws IOException { + super.onMessageReceived(message, session); + // + System.out.println(session + "--->" + message); + } + + @Override + public void onExceptionCaught(Throwable e, INioSession session) throws IOException { + System.out.println(session + "--->" + e); + } + } + +##### UDP客户端 + + @Client(implClass = NioUdpClient.class, codec = TextLineCodec.class) + public class UdpClient extends NioUdpListener { + + public Object onSessionReady() throws IOException { + return "Hello from client."; + } + + public Object onMessageReceived(InetSocketAddress sourceAddr, Object message) throws IOException { + System.out.println(sourceAddr + "--->" + message); + return null; + } + + public void onExceptionCaught(InetSocketAddress sourceAddr, Throwable e) throws IOException { + System.out.println(sourceAddr + "--->" + e); + } + } + +## 客户端和服务端对象的使用 + +YMP框架启动时将自动扫描并加载声明了`@Server`和`@Client`注解的类,并根据注解设置和对应的参数配置进行客户端或服务端对象的初始化,但此时的客户端和服务端程序并没有直正执行,需要手动完成启动动作,代码如下: + +- 示例一:启动所有已加载的客户端、服务端服务 + + public static void main(String[] args) throws Exception { + YMP.get().init(); + // + Servs.get().startup(); + } + +- 示例二:获取指定的客户端或服务端服务,启动服务并向服务端发送消息 + + public static void main(String[] args) throws Exception { + YMP.get().init(); + + // 获取服务端实例对象 + NioUdpServer _serv = Servs.get().getServer(UdpServer.class); + // 启动服务 + _serv.start(); + + // 获取客户端实例对象 + NioUdpClient _c = Servs.get().getClient(UdpClient.class); + // 连接到远程服务 + _c.connect(); + // 通过客户端对象向服务端发送消息 + _c.send("Message from Client."); + } + +## 通过代码创建并配置客户端和服务端对象 + +上述内容主要说明如果基于框架配置文件初始化、启动和调用TCP、UDP服务端与客户端对象,下面阐述的是通过手工编码方式完成客户端或服务端的配置、启动和调用过程,此方法不需要配置文件支持: + +### 示例代码 + +假设,我们通过手工方法创建YMP实例对象,注册`Serv`模块(也可以直接通过`YMP.get()`获取实例)并完成初始化,代码如下: + + YMP owner = new YMP(ConfigBuilder.create() + .proxyFactory(new NoOpProxyFactory()) + .beanLoader(new AbstractBeanLoader() { + @Override + public void load(IBeanFactory beanFactory, IBeanFilter filter) throws Exception { + // Nothing... + } + }).developMode(true).runEnv(IConfig.Environment.PRODUCT).build()); + owner.registerModule(new Servs()); + owner.init(); + +- 服务端 + + NioServer server = Servs.get().buildServer(DefaultServerCfg.create().serverHost("localhost").port(8281).build(), new TextLineCodec(), new NioServerListener() { + @Override + public void onSessionAccepted(INioSession session) throws IOException { + super.onSessionAccepted(session); + // + System.out.println("Session accepted: " + session); + } + + @Override + public void onMessageReceived(Object message, INioSession session) throws IOException { + // 输出接收到的消息 + System.out.println("Message received: " + message); + // 向客户端发送消息 + session.send("Hi, guys!"); + } + + @Override + public void onAfterSessionClosed(INioSession session) throws IOException { + System.out.println("Session closed: " + session); + } + + @Override + public void onBeforeSessionClosed(INioSession session) throws IOException { + System.out.println("Session closing: " + session); + } + }); + server.start(); + +- 客户端 + + IClient client = Servs.get(owner) + .buildClient(DefaultClientCfg.create() + .remoteHost("localhost").port(8281).build(), new TextLineCodec(), new DefaultReconnectService(), new DefaultHeartbeatService(), new NioClientListener() { + @Override + public void onSessionConnected(INioSession session) throws IOException { + super.onSessionConnected(session); + // + session.send("Hello!"); + } + + @Override + public void onMessageReceived(Object message, INioSession session) throws IOException { + super.onMessageReceived(message, session); + // + System.out.println(session + "--->" + message); + } + + @Override + public void onExceptionCaught(Throwable e, INioSession session) throws IOException { + System.out.println(session + "--->" + e); + } + + }); + client.connect(); + +## 会话管理器 + +会话管理器的作用是帮助TCP、UDP服务端管理已连接的客户端会话,目前主要功能包括: + +- 空闲会话检查:当会话在设定的时间内与服务器之间无任何通讯时,此会话将被关闭并从会话管理器中移除; + +- 流量速度统计:通过记录客户端与服务端的消息收发数量,能够计算出消息处理的实时速度、平均速度、最大及最小速度值; + +- 向客户端主动发送消息:通过调用会话管理器实例对象方法,可以根据业务需要主动向指定会话发送消息; + +- 移除客户端会话:通过调用会话管理器实例对象方法,可以将指定标识的会话关闭并将其移除; + + +### 示例程序: + +- TCP会话管理器示例: + + ``` + public class TcpSessionListener implements INioSessionListener { + + private static final Log _LOG = LogFactory.getLog(TcpSessionListener.class); + + public static void main(String[] args) throws Exception { + // 初始化YMP框架 + YMP.get().init(); + // 创建服务端配置 + IServerCfg _serverCfg = DefaultServerCfg.create() + .selectorCount(10) + .serverHost("localhost") + .port(8281) + .keepAliveTime(60000).build(); + // 通过会话管理器创建服务 (设置会话空闲时间为30秒) + NioSessionManager _manager = new NioSessionManager(_serverCfg, new NioStringCodec(), new TcpSessionListener(), 30000L); + // 设置空闲会话检查服务 + _manager.idleChecker(new DefaultSessionIdleChecker()); + // 设置流量速度计数器 + _manager.speedometer(new Speedometer()); + // 初始化并启动服务 + _manager.init(Servs.get()); + + // ------------------- + + // 遍历会话并向其发送消息 + for (NioSessionWrapper _session : _manager.sessionWrappers()) { + _manager.sendTo(_session.getId(), "Send message from server."); + } + // 当前会话总数 + System.out.println("Current session count: " + _manager.sessionCount()); + // 将已连接的客户端会话从管理器中移除 + for (NioSessionWrapper _session : _manager.sessionWrappers()) { + _manager.closeSessionWrapper(_session); + } + // 销毁会话管理器 + _manager.destroy(); + } + + @Override + public void onSessionRegistered(NioSessionWrapper session) throws IOException { + _LOG.info("onSessionRegistered: " + session.getId()); + } + + @Override + public void onSessionAccepted(NioSessionWrapper session) throws IOException { + _LOG.info("onSessionAccepted: " + session.getId()); + } + + @Override + public void onBeforeSessionClosed(NioSessionWrapper session) throws IOException { + _LOG.info("onBeforeSessionClosed: " + session.getId()); + } + + @Override + public void onAfterSessionClosed(NioSessionWrapper session) throws IOException { + _LOG.info("onAfterSessionClosed: " + session.getId()); + } + + @Override + public void onMessageReceived(String message, NioSessionWrapper session) throws IOException { + _LOG.info("onMessageReceived: " + message + " from " + session.getId()); + } + + @Override + public void onExceptionCaught(Throwable e, NioSessionWrapper session) throws IOException { + _LOG.info("onExceptionCaught: " + e.getMessage() + " -- " + session.getId()); + } + + @Override + public void onSessionIdleRemoved(NioSessionWrapper sessionWrapper) { + _LOG.info("onSessionIdleRemoved: " + sessionWrapper.getId()); + } + } + ``` + +- UDP会话管理器示例: + + ``` + public class UdpSessionListener implements INioUdpSessionListener { + + private static final Log _LOG = LogFactory.getLog(UdpSessionListener.class); + + public static void main(String[] args) throws Exception { + // 初始化YMP框架 + YMP.get().init(); + // 创建服务端配置 + IServerCfg _serverCfg = DefaultServerCfg.create() + .selectorCount(10) + .serverHost("localhost") + .port(8281).build(); + // 通过会话管理器创建服务 (设置会话空闲时间为30秒) + NioUdpSessionManager _manager = new NioUdpSessionManager(_serverCfg, new NioStringCodec(), new UdpSessionListener(), 30000L); + // 设置空闲会话检查服务 + _manager.idleChecker(new DefaultSessionIdleChecker()); + // 设置流量速度计数器 + _manager.speedometer(new Speedometer()); + // 初始化并启动服务 + _manager.init(Servs.get()); + + // ------------------- + + // 遍历会话并向其发送消息 + for (NioUdpSessionWrapper _session : _manager.sessionWrappers()) { + _manager.sendTo(_session.getId(), "Send message from server."); + } + // 当前会话总数 + System.out.println("Current session count: " + _manager.sessionCount()); + // 将已连接的客户端会话从管理器中移除 + for (NioUdpSessionWrapper _session : _manager.sessionWrappers()) { + _manager.closeSessionWrapper(_session); + } + // 销毁会话管理器 + _manager.destroy(); + } + + @Override + public Object onMessageReceived(NioUdpSessionWrapper sessionWrapper, String message) throws IOException { + _LOG.info("onMessageReceived: " + message + " from " + sessionWrapper.getId()); + // 当收到消息后,可以直接向客户端回复消息 + return "Hi, " + sessionWrapper.getId(); + } + + @Override + public void onExceptionCaught(NioUdpSessionWrapper sessionWrapper, Throwable e) throws IOException { + _LOG.info("onExceptionCaught: " + e.getMessage() + " -- " + sessionWrapper.getId()); + } + + @Override + public void onSessionIdleRemoved(NioUdpSessionWrapper sessionWrapper) { + _LOG.info("onSessionIdleRemoved: " + sessionWrapper.getId()); + } + } + ``` + +> **注意**: +> +> - 通过手工编码方式创建的服务端或客户端实例对象将不被框架管理,需要开发者手动调用关闭方法(如:`server.close()`或`client.close()`)来释放资源。 +> - YMP框架初始化后,若使用`try...finally`执行`YMP.get().destroy()`销毁动作,则服务启动后将立即被停止。 diff --git a/misc/site/guide/validation.md b/misc/site/guide/validation.md new file mode 100755 index 00000000..bb9890dd --- /dev/null +++ b/misc/site/guide/validation.md @@ -0,0 +1,260 @@ +--- +sidebarDepth: 2 +--- + +# 验证(Validation) + +验证模块是服务端参数有效性验证工具,采用注解声明方式配置验证规则,更简单、更直观、更友好,支持方法参数和类成员属性验证,支持验证结果国际化I18N资源绑定,支持自定义验证器,支持多种验证模式; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-validation + + + +> **注**:在项目的pom.xml中添加上述配置,该模块已经默认引入核心包依赖,无需重复配置。 + + +## 默认验证器及参数说明 + +### @VCompare + +​比较两个参数值,使用场景如:新密码与重复新密码两参数值是否一致的比较; + + > cond:比较条件,可选EQ、NOT_EQ、GT、GT_EQ、LT和LT_EQ,默认为EQ; + > + > with:与之比较的参数名称; + > + > withLabel:与之比较的参数标签名称 (用于在验证消息里显示的名称),默认为空; + > + > msg:自定义验证消息,默认为空; + +### @VDateTime + +日期类型参数验证; + + > pattern:日期格式字符串,默认为yyyy-MM-dd HH:mm:ss + > + > msg:自定义验证消息,默认为空; + +### @VEmail + +邮箱地址格式验证; + + > msg:自定义验证消息,默认为空; + +### @VLength + +字符串长度验证; + + > min:设置最小长度,0为不限制; + > max:设置最大长度,0为不限制; + > msg:自定义验证消息,默认为空; + +### @VNumeric + +数值类型参数验证; + + > min:设置最小值,0为不限制; + > max:设置最大值,0为不限制; + > msg:自定义验证消息,默认为空; + +### @VRegex + +正则表达式验证; + + > regex:正则表达式; + > + > msg:自定义验证消息,默认为空; + +### @VRequired + +必填项验证; + + > msg:自定义验证消息,默认为空; + +**注**: + +- 以上注解中的msg参数即可以是输出的消息内容,也可以是国际化资源文件中的键; +- 验证器是按注解声明的顺序执行的,请一定要注意!!! + +## 默认国际化资源文件内容 + +验证框架的默认国际化资源文件名称为 **validation.properties**,其内容如下: + + ymp.validation.compare_not_eq={0} can not eq {1}. + ymp.validation.compare_eq={0} must be eq {1}. + + ymp.validation.datetime={0} not a valid datetime. + + ymp.validation.email={0} not a valid email address. + + ymp.validation.length_between={0} length must be between {1} and {2}. + ymp.validation.length_min={0} length must be gt {1}. + ymp.validation.length_max={0} length must be lt {1}. + + ymp.validation.numeric={0} not a valid numeric. + ymp.validation.numeric_between={0} numeric must be between {1} and {2}. + ymp.validation.numeric_min={0} numeric must be gt {1}. + ymp.validation.numeric_max={0} numeric must be lt {1}. + + ymp.validation.regex={0} regex not match. + + ymp.validation.required={0} must be required. + +## 验证框架使用示例 + +- 示例代码: + + @Validation(mode = Validation.MODE.FULL) + public class UserBase { + + @VRequired(msg = "{0}不能为空") + @VLength(min = 3, max = 16, msg = "{0}长度必须在3到16之间") + @VField(label = "用户名称") + private String username; + + @VRequired + @VLength(max = 32) + private String password; + + @VRequired + @VCompare(cond = VCompare.Cond.EQ, with = "password") + private String repassword; + + @VModel + @VField(name = "ext") + private UserExt userExt; + + // + // 此处省略了Get/Set方法 + // + } + + public class UserExt { + + @VLength(max = 10) + private String sex; + + @VRequired + @VNumeric(min = 18, max = 30) + private int age; + + @VRequried + @VEmail + private String email; + + // + // 此处省略了Get/Set方法 + // + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + Map _params = new HashMap(); + _params.put("username", "lz"); + _params.put("password", 1233); + _params.put("repassword", "12333"); + _params.put("ext.age", "17"); + _params.put("ext.email", "@163.com"); + + Map _results = Validations.get().validate(UserBase.class, _params); + // + for (Map.Entry _entry : _results.entrySet()) { + System.out.println(_entry.getValue().getMsg()); + } + } finally { + YMP.get().destroy(); + } + } + +- 执行结果: + + username : 用户名称长度必须在3到16之间 + repassword : repassword must be eq password. + ext.age : ext.age numeric must be between 30 and 18. + ext.email : ext.email not a valid email address. + +> 功能注解说明: + +> - `@Validation`:验证模式配置,默认为NORMAL; +> + NORMAL - 短路式验证,即出现验证未通过就返回验证结果; +> + FULL - 对目标对象属性进行全部验证后返回全部验证结果; +> +> - `@VField`:自定义待验证的成员或方法参数名称; +> + name:自定义参数名称,在嵌套验证时上下层参数以'.'分隔; +> + label:自定义参数标签名称,若@VField嵌套使用时功能将不可用; +> +> - `@VModel`:声明目标对象是否为JavaBean对象,将执行对象嵌套验证; + +## 自定义验证器 + +写代码前先了解一个新的注解`@Validator`,它的作用是声明一个类为验证器,它的参数需要绑定自定义验证器对应的注解,这个注解的作用与`@VRequried`等注解是一样的,开发人员可以通过该注解配置验证规则; + +本例中,我们创建一个简单的自定义验证器,用来验证当前用户输入的邮箱地址是否已存在; + +- 创建自定义验证器注解: + + @Target({ElementType.FIELD, ElementType.PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface VEmailCanUse { + + /** + * @return 自定义验证消息 + */ + String msg() default ""; + } + +- 实现IValidator接口并声明@Validator注解: + + @Validator(VEmailCanUse.class) + public class EmailCanUseValidator implements IValidator { + + public ValidateResult validate(ValidateContext context) { + ValidateResult _result = null; + if (context.getParamValue() != null) { + // 假定邮箱地址已存在 + VEmailCanUse _anno = (VEmailCanUse) context.getAnnotation(); + _result = new ValidateResult(context.getParamName(), StringUtils.defaultIfBlank(_anno.msg(), "邮箱地址已存在")); + } + return _result; + } + } + +- 测试代码: + + public class VEmailCanUseBean { + + @VRequried + @VEmail + @VEmailCanUse + private String email; + + // + // 此处省略了Get/Set方法 + // + } + + public static void main(String[] args) throws Exception { + YMP.get().init(); + try { + Map _params = new HashMap(); + _params.put("ext.email", "demo@163.com"); + + Map _results = Validations.get().validate(VEmailCanUseBean.class, _params); + // + for (Map.Entry _entry : _results.entrySet()) { + System.out.println(_entry.getKey() + " : " + _entry.getValue().getMsg()); + } + } finally { + YMP.get().destroy(); + } + } + +- 执行结果: + + ext.email : 邮箱地址已存在 diff --git a/misc/site/guide/webmvc.md b/misc/site/guide/webmvc.md new file mode 100755 index 00000000..a5c93996 --- /dev/null +++ b/misc/site/guide/webmvc.md @@ -0,0 +1,1502 @@ +--- +sidebarDepth: 2 +--- + +# WebMVC + +WebMVC模块在YMP框架中是除了JDBC模块以外的另一个非常重要的模块,集成了YMP框架的诸多特性,在功能结构的设计和使用方法上依然保持一贯的简单风格,同时也继承了主流MVC框架的基因,对于了解和熟悉SSH等框架技术的开发人员来说,上手极其容易,毫无学习成本。 + +其主要功能特性如下: + +- 标准MVC实现,结构清晰,完全基于注解方式配置简单; +- 支持约定模式,无需编写控制器代码,直接匹配并执行视图; +- 支持多种视图技术(JSP、Freemarker、Velocity、Text、HTML、JSON、Binary、Forward、Redirect、HttpStatus、Beetl等); +- 支持RESTful模式及URL风格; +- 支持请求参数与控制器方法参数的自动绑定; +- 支持参数有效性验证; +- 支持控制器方法的拦截; +- 支持注解配置控制器请求路由映射; +- 支持自动扫描控制器类并注册; +- 支持事件和异常的自定义处理; +- 支持I18N资源国际化; +- 支持控制器方法和视图缓存; +- 支持控制器参数转义; +- 支持插件扩展; + +## Maven包依赖 + + + net.ymate.platform + ymate-platform-webmvc + + + +> **注**:在项目的pom.xml中添加上述配置,该模块已经默认引入核心包、验证框架包和缓存包的依赖,无需重复配置。 +> 若不想启用缓存服务只需在`ymp-conf.properties`中增加排除caches模块配置,如:`ymp.excluded_modules=cache` + +## 模块初始化 + +在Web程序中监听器(Listener)是最先被容器初始化的,所以WebMVC模块是由监听器负责对YMP框架进行初始化: + +> 监听器(Listener):net.ymate.platform.webmvc.support.WebAppEventListener + +处理浏览器请求并与模块中控制器匹配、路由的过程可分别由过滤器(Filter)和服务端程序(Servlet)完成: + +> 过滤器(Filter):net.ymate.platform.webmvc.support.DispatchFilter + +> 服务端程序(Servlet):net.ymate.platform.webmvc.support.DispatchServlet + +首先看一下完整的web.xml配置文件: + + + + + + net.ymate.platform.webmvc.support.WebAppEventListener + + + + DispatchFilter + net.ymate.platform.webmvc.support.DispatchFilter + + + DispatchFilter + /* + REQUEST + FORWARD + + + + + + index.html + index.jsp + + + +## 模块配置 + +WebMVC模块的基本初始化参数配置: + + #------------------------------------- + # 基本初始化参数 + #------------------------------------- + + # 控制器请求映射路径分析器,可选值为已知分析器名称或自定义分析器类名称,默认为default + ymp.configs.webmvc.request_mapping_parser_class= + + # 控制器请求处理器,可选值为已知处理器名称或自定义处理器类名称,自定义类需实现net.ymate.platform.webmvc.IRequestProcessor接口,默认为default,目前支持已知处理器[default|json|xml|...] + ymp.configs.webmvc.request_processor_class= + + # 异常错误处理器,可选参数,此类需实现net.ymate.platform.webmvc.IWebErrorProcessor接口,默认值为net.ymate.platform.webmvc.impl.DefaultWebErrorProcessor + ymp.configs.webmvc.error_processor_class= + + # 缓存处理器,可选参数,此类需实现net.ymate.platform.webmvc.IWebCacheProcessor接口 + ymp.configs.webmvc.cache_processor_class= + + # 国际化资源文件存放路径,可选参数,默认值为${root}/i18n/ + ymp.configs.webmvc.i18n_resources_home= + + # 国际化资源文件名称,可选参数,默认值为messages + ymp.configs.webmvc.i18n_resource_name= + + # 国际化语言设置参数名称,可选参数,默认值为_lang + ymp.configs.webmvc.i18n_language_param_name= + + # 默认字符编码集设置,可选参数,默认值为UTF-8 + ymp.configs.webmvc.default_charset_encoding= + + # 默认Content-Type设置,可选参数,默认值为text/html + ymp.configs.webmvc.default_content_type= + + # 请求忽略正则表达式,可选参数,默认值为^.+\.(jsp|jspx|png|gif|jpg|jpeg|js|css|swf|ico|htm|html|eot|woff|woff2|ttf|svg|map)$ + ymp.configs.webmvc.request_ignore_regex= + + # 请求方法参数名称,可选参数, 默认值为_method + ymp.configs.webmvc.request_method_param= + + # 请求路径前缀,可选参数,默认值为空 + ymp.configs.webmvc.request_prefix= + + # 请求参数转义模式是否开启(开启状态时,控制器方法的所有参数将默认支持转义,可针对具体控制器主法或参数设置忽略转义操作),可选参数,默认值为false + ymp.configs.webmvc.parameter_escape_mode= + + # 执行请求参数转义顺序,可选参数,取值范围:before(参数验证之前)和after(参数验证之后),默认值为after + ymp.configs.webmvc.parameter_escape_order= + + # 控制器视图文件基础路径(必须是以 '/' 开始和结尾,默认值为/WEB-INF/templates/) + ymp.configs.webmvc.base_view_path= + +**说明**:在服务端程序Servlet方式的请求处理中,请求忽略正则表达式`request_ignore_regex`参数无效; + +WebMVC模块的扩展参数配置: + + #------------------------------------- + # WebMVC扩展配置参数 + #------------------------------------- + + # 控制器请求URL后缀参数名称,默认值为空 + ymp.params.webmvc.request_suffix= + + # 服务名称参数, 默认值: request.getServerName(); + ymp.params.webmvc.server_name= + + # 系统异常分析是否关闭,默认值为false + ymp.params.webmvc.exception_analysis_disabled= + + # 系统错误消息是否指定ContentType响应头,默认值为false + ymp.params.webmvc.error_with_content_type= + + # 默认异常响应视图格式, 默认值: "", 可选范围: json|xml + ymp.params.webmvc.error_default_view_format= + + # 常信息视图文件名称,默认值为error.jsp + ymp.params.webmvc.error_view= + + # 验证结果消息模板参数名称, 默认值: "${items}" + ymp.params.webmvc.validation_template_element= + + # 验证结果消息项模板参数名称, 默认值: "${message}
" + ymp.params.webmvc.validation_template_item= + + # 重定向主页URL地址参数名称, 默认值: "" + ymp.params.webmvc.redirect_home_url= + + # 自定义重定向URL地址参数名称, 默认值: "" + ymp.params.webmvc.redirect_custom_url= + +## 通过代码手工初始化模块示例 + + // 创建YMP实例 + YMP owner = new YMP(ConfigBuilder.create( + // 设置WebMVC模块配置 + ModuleCfgProcessBuilder.create().putModuleCfg( + WebMvcModuleConfigurable.create() + .requestProcessorClass("json") + .errorProcessorClass(DefaultWebErrorProcessor.class) + .cacheProcessorClass(WebCacheProcessor.class) + .conventionMode(false) + .defaultCharsetEncoding("UTF-8") + .i18nResourceName("messages") + .i18nResourcesHome("${root}/i18n/")).build()) + .proxyFactory(new DefaultProxyFactory()) + .i18nEventHandler(new I18NWebEventHandler()) + // 扩展配置 + .param(IWebMvcModuleCfg.PARAMS_ERROR_DEFAULT_VIEW_FORMAT, "json") + .param(IWebMvcModuleCfg.PARAMS_ERROR_VIEW, "error.jsp") + .param(IWebMvcModuleCfg.PARAMS_EXCEPTION_ANALYSIS_DISABLED, "false") + .developMode(true) + .runEnv(IConfig.Environment.PRODUCT).build()); + // 向容器注册模块 + owner.registerModule(WebMVC.class); + // 执行框架初始化 + owner.init(); + +## 模块事件 + +WebEvent事件枚举对象包括以下事件类型: + +|事务类型|说明| +|---|---| +|SERVLET_CONTEXT_INITED|容器初始化事件| +|SERVLET_CONTEXT_DESTROYED|容器销毁事件| +|SERVLET_CONTEXT_ATTR_ADDED|| +|SERVLET_CONTEXT_ATTR_REMOVEED|| +|SERVLET_CONTEXT_ATTR_REPLACED|| +|SESSION_CREATED|会话创建事件| +|SESSION_DESTROYED|会话销毁事件| +|SESSION_ATTR_ADDED|| +|SESSION_ATTR_REMOVEED|| +|SESSION_ATTR_REPLACED|| +|REQUEST_INITED|请求初始化事件| +|REQUEST_DESTROYED|请求销毁事件| +|REQUEST_ATTR_ADDED|| +|REQUEST_ATTR_REMOVEED|| +|REQUEST_ATTR_REPLACED|| +|REQUEST_RECEIVED|接收控制器方法请求事件| +|REQUEST_COMPLETED|完成控制器方法请求事件| + +## 控制器(Controller) + +控制器(Controller)是MVC体系中的核心,它负责处理浏览器发起的所有请求和决定响应内容的逻辑处理,控制器就是一个标准的Java类,不需要继承任何基类,通过类中的方法向外部暴露接口,该方法的返回结果将决定向浏览器响应的具体内容; + +下面通过示例编写WebMVC模块中的控制器: + + @Controller + public class DemoController { + + @RequestMapping("/sayhi") + public IView sayHi() { + return View.textView("Hi, YMPer!"); + } + } + +启动Tomcat服务并访问`http://localhost:8080/sayhi`,得到的输出结果将是:`Hi, YMPer!` + +从以上代码中看到有两个注解,分别是: + +- @Controller:声明一个类为控制器,框架在启动时将会自动扫描所有声明该注解的类并注册为控制器; + + > name:控制器名称,默认为“”(该参数暂时未被使用); + > + > singleton:指定控制器是否为单例,默认为true; + +- @RequestMapping:声明控制器请求路径映射,作用域范围:类或方法; + + > value:控制器请求路径映射,必选参数; + > + > method[]:允许的请求方式,默认为GET方式,取值范围:GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE; + > + > header[]:请求头中必须存在的头名称; + > + > param[]:请求中必须存在的参数名称; + +示例一: + +创建非单例控制器,其中的控制器方法规则如下: +> 1. 控制器方法仅支持POST和PUT方式访问; +> 2. 请求头参数中必须包含x-requested-with=XMLHttpRequest(即判断是否AJAX请求); +> 3. 请求参数中必须存在name参数; + + @Controller(singleton = false) + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping(value = "/sayhi", + method = {Type.HttpMethod.POST, Type.HttpMethod.PUT}, + header = {"x-requested-with=XMLHttpRequest"}, + param = {"name=*"}) + public IView sayHi() { + return View.textView("Hi, YMPer!"); + } + } + +示例说明: +> 本例主要展示了如何使用@Controller和@RequestMapping注解来对控制器和控制器方法对进配置; +> +> 控制器方法必须使用public修饰,否则无效; +> +> 由于控制器上也声明了@RequestMapping注解,所以控制器方法的请求路径映射将变成:/demo/sayhi; + +示例二: + +上例中展示了对请求的一些控制,下面展示如何对响应结果进行控制,规则如下: + +> 1. 通过注解设置响应头参数: +> - from = "china" +> - age = 18 +> 2. 通过注解设置控制器返回视图及内容:"Hi, YMPer!" + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/sayhi") + @ResponseView(value = "Hi, YMPer!", type = Type.View.TEXT) + @ResponseHeader({ + @Header(name = "from", value = "china"), + @Header(name = "age", value = "18", type = Type.HeaderType.INT)}) + public void sayHi() { + } + } + +本例中用到了三个注解: + +- @ResponseView:声明控制器方法默认返回视图对象, 仅在方法无返回值或返回值无效时使用 + + > name:视图模板文件路径,默认为""; + > + > type:视图文件类型,默认为Type.View.NULL; + +- @ResponseHeader:设置控制器方法返回结果时增加响应头参数; + + > value[]:响应头@Header参数集合; + +- @Header:声明一个请求响应Header键值对,仅用于参数传递; + + > name:响应头参数名称,必选参数; + > + > value:响应头参数值,默认为""; + > + > type:响应头参数类型,支持STRING, INI, DATE,默认为Type.HeaderType.STRING; + + +## 控制器参数(Parameter) + +WebMVC模块不但让编写控制器变得非常简单,处理请求参数也变得更加容易!WebMVC会根据控制器方法参数或类成员的注解配置,自动转换与方法参数或类成员对应的数据类型,参数的绑定涉及以下注解: + +### 基本参数注解 + +- @RequestParam:绑定请求中的参数; + +- @RequestHeader:绑定请求头中的参数变量; + +- @CookieVariable:绑定Cookie中的参数变量; + +上面三个注解拥有相同的参数: + +> value:参数名称,若未指定则默认采用方法参数变量名; +> +> prefix:参数名称前缀,默认为""; +> +> defaultValue:指定参数的默认值,默认为""; + +示例代码: + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/param") + public IView testParam(@RequestParam String name, + @RequestParam(defaultValue = "18") Integer age, + @RequestParam(value = "name", prefix = "user") String username, + @RequestHeader(defaultValue = "BASIC") String authType, + @CookieVariable(defaultValue = "false") Boolean isLogin) { + + System.out.println("AuthType: " + authType); + System.out.println("IsLogin: " + isLogin); + return View.textView("Hi, " + name + ", UserName: " + username + ", Age: " + age); + } + } + +通过浏览器访问URL测试: + + http://localhost:8080/demo/param?name=webmvc&user.name=ymper + +执行结果: + + 控制台输出: + AuthType: BASIC + IsLogin: false + + 浏览器输出: + Hi, webmvc, UserName: ymper, Age: 18 + +### 特别的参数注解 + +- @PathVariable:绑定请求映射中的路径参数变量; + > value:参数名称,若未指定则默认采用方法参数变量名; + + 示例代码: + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/path/{name}/{age}") + public IView testPath(@PathVariable String name, + @PathVariable(value = "age") Integer age, + @RequestParam(prefix = "user") String sex) { + + return View.textView("Hi, " + name + ", Age: " + age + ", Sex: " + sex); + } + } + + 通过浏览器访问URL测试: + + http://localhost:8080/demo/path/webmvc/20?user.sex=F + + 执行结果: + + Hi, webmvc, Age: 20, Sex: F + + > **注意**: + > + > + 默认请求解析`request_mapping_parser_class=default`时,以下均能正确解析: + > + > - 正确:/path/{name}/{age} + > + > - 正确:/path/{name}/age/{sex} + > + > + 可以通过实现IRequestMappingParser接口自行定义控制器请求规则; + +- @ModelBind:值对象参数绑定注解; + > prefix:绑定的参数名称前缀,可选参数,默认为""; + + 示例代码: + + public class DemoVO { + + @PathVariable + private String name; + + @RequestParam + private String sex; + + @RequestParam(prefix = "ext") + private Integer age; + + // 省略Get和Set方法 + } + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/bind/{demo.name}") + public IView testBind(@ModelBind(prefix = "demo") DemoVO vo) { + String _str = "Hi, " + vo.getName() + ", Age: " + vo.getAge() + ", Sex: " + vo.getSex(); + return View.textView(_str); + } + } + + 通过浏览器访问URL测试: + + http://localhost:8080/demo/bind/webmvc?demo.sex=F&demo.ext.age=20 + + 执行结果: + + Hi, webmvc, Age: 20, Sex: F + +- @ParameterEscape:控制器方法参数转义注解; + + 可以通过WebMVC模块配置参数`parameter_escape_order`设定是在控制器方法参数执行验证之前还是之后执行参数转义动作,参数取值范围为`before`或`after`,默认为`after`即参数验证之后进行转义; + + > scope:字符串参数转义范围,默认为Type.EscapeScope.DEFAULT; + > + > - 取值范围包括:JAVA, JS, HTML, XML, SQL, CSV, DEFAULT; + > - 默认值DEFAULT,它完成了对SQL和HTML两项转义; + > + > skiped:通知父级注解当前方法或参数的转义操作将被忽略,默认为false; + > + > processor:自定义字符串参数转义处理器; + > + > - 可以通过IParameterEscapeProcessor接口实现自定义的转义逻辑; + > - 默认实现为DefaultParameterEscapeProcessor; + + 示例代码一: + + @Controller + @RequestMapping("/demo") + @ParameterEscape + public class DemoController { + + @RequestMapping("/escape") + public IView testEscape(@RequestParam String content, + @ParameterEscape(skiped = true) @RequestParam String desc) { + + System.out.println("Content: " + content); + System.out.println("Desc: " + desc); + return View.nullView(); + } + } + + // 或者:(两段代码执行结果相同) + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/escape") + @ParameterEscape + public IView testEscape(@RequestParam String content, + @ParameterEscape(skiped = true) @RequestParam String desc) { + + System.out.println("Content: " + content); + System.out.println("Desc: " + desc); + return View.nullView(); + } + } + + 通过浏览器访问URL测试: + + http://localhost:8080/demo/escape?content=

content$

&desc= + + 执行结果:(控制台输出) + + Content: <p>content$<br><script>alert("hello");</script></p> + Desc: + + > 示例一说明: + > + > - 由于控制器类被声明了@ParameterEscape注解,代表整个控制器类中所有的请求参数都需要被转义,因此参数content的内容被成功转义; + > - 由于参数desc声明的@ParameterEscape注解中skiped值被设置为true,表示跳过上级设置,因此参数内容未被转义; + + 示例代码二: + + @Controller + @RequestMapping("/demo") + @ParameterEscape + public class DemoController { + + @RequestMapping("/escape2") + @ParameterEscape(skiped = true) + public IView testEscape2(@RequestParam String content, + @ParameterEscape @RequestParam String desc) { + + System.out.println("Content: " + content); + System.out.println("Desc: " + desc); + return View.nullView(); + } + } + + 通过浏览器访问URL测试: + + http://localhost:8080/demo/escape2?content=

content$

&desc= + + 执行结果:(控制台输出) + + Content:

content$

+ Desc: <script>alert("hello");</script> + + > 示例二说明: + > + > - 虽然控制器类被声明了@ParameterEscape注解,但控制器方法通过skiped设置跳过转义,这表示被声明的方法参数内容不进行转义操作,因此参数content的内容未被转义; + > - 由于参数desc声明了@ParameterEscape注解,表示该参数需要转义,因此参数内容被成功转义; + > + > **注意**:当控制器类和方法都声明了@ParameterEscape注解时,则类上声明的注解将视为无效; + +## 非单例控制器的特殊用法 + +单例控制器与非单例控制器的区别: + +- 单例控制器类在WebMVC模块初始化时就已经实例化; +- 非单例控制器类则是在每次接收到请求时都将创建实例对象,请求结束后该实例对象被释放; + +基于以上描述,非单例控制器是可以通过类成员来接收请求参数,示例代码如下: + + @Controller(singleton = false) + @RequestMapping("/demo") + public class DemoController { + + @RequestParam + private String content; + + @RequestMapping("/sayHi") + public IView sayHi(@RequestParam String name) { + return View.textView("Hi, " + name + ", Content: " + content); + } + } + +通过浏览器访问URL测试: + + http://localhost:8080/demo/sayHi?name=YMPer&content=Welcome! + +此示例代码的执行结果: + + Hi, YMPer, Content: Welcome! + +> **注意**:在单例模式下,WebMVC模块将忽略为控制器类成员赋值,同时也建议在单例模式下不要使用成员变量做为参数,在并发多线程环境下会发生意想不到的问题!! + +## 环境上下文对象(WebContext) + +为了让开发人员能够随时随地获取和使用Request、Response、Session等Web容器对象,YMP框架在WebMVC模块中提供了一个名叫WebContext的Web环境上下文封装类,简单又实用,先了解一下提供的方法: + +直接获取Web容器对象: +> +> - 获取ServletContext对象: +> +> WebContext.getServletContext(); +> +> - 获取HttpServletRequest对象: +> +> WebContext.getRequest(); +> +> - 获取HttpServletResponse对象: +> +> WebContext.getResponse(); +> +> - 获取PageContext对象: +> +> WebContext.getPageContext(); + +获取WebMVC容器对象: + +> - 获取IRequestContext对象: +> +> WebContext.getRequestContext(); +> +> > WebMVC请求上下文接口,主要用于分析请求路径及存储相关参数; +> +> - 获取WebContext对象实例: +> +> WebContext.getContext(); +> + +WebContext将Application、Session、Request等Web容器对象的属性转换成Map映射存储,同时向Map的赋值也将自动同步至对象的Web容器对象中,起初的目的是为了能够方便代码移植并脱离Web环境依赖进行开发测试(功能参考Struts2): + +> - WebContext.getContext().getApplication(); +> +> - WebContext.getContext().getSession(); +> +> - WebContext.getContext().getAttribute(Type.Context.REQUEST); +> +> > 原本可以通过WebContext.getContext().getRequest方法直接获取的,但由于设计上的失误,方法名已被WebContext.getRequest()占用,若变更方法名受影响的项目太多,所以只好委屈它了:D,后面会介绍更多的辅助方法来操作Request属性,所以可以忽略它的存在! +> +> - WebContext.getContext().getAttributes(); +> +> - WebContext.getContext().getLocale(); +> +> - WebContext.getContext().getOwner(); +> +> - WebContext.getContext().getParameters(); +> + +WebContext操作Application的辅助方法: + +> - boolean getApplicationAttributeToBoolean(String name); +> +> - int getApplicationAttributeToInt(String name); +> +> - long getApplicationAttributeToLong(String name); +> +> - String getApplicationAttributeToString(String name); +> +> - \ T getApplicationAttributeToObject(String name); +> +> - WebContext addApplicationAttribute(String name, Object value) +> + +WebContext操作Session的辅助方法: + +> - boolean getSessionAttributeToBoolean(String name); +> +> - int getSessionAttributeToInt(String name); +> +> - long getSessionAttributeToLong(String name); +> +> - String getSessionAttributeToString(String name); +> +> - \ T getSessionAttributeToObject(String name); +> +> - WebContext addSessionAttribute(String name, Object value) +> + +WebContext操作Request的辅助方法: + +> - boolean getRequestAttributeToBoolean(String name); +> +> - int getRequestAttributeToInt(String name); +> +> - long getRequestAttributeToLong(String name); +> +> - String getRequestAttributeToString(String name); +> +> - \ T getRequestAttributeToObject(String name); +> +> - WebContext addRequestAttribute(String name, Object value) +> + +WebContext操作Parameter的辅助方法: + +> - boolean getParameterToBoolean(String name); +> +> - int getParameterToInt(String name) +> +> - long getParameterToLong(String name); +> +> - String getParameterToString(String name); +> + +WebContext操作Attribute的辅助方法: + +> - \ T getAttribute(String name); +> +> - WebContext addAttribute(String name, Object value); +> + + +WebContext获取IUploadFileWrapper上传文件包装器: + +> - IUploadFileWrapper getUploadFile(String name); +> +> - IUploadFileWrapper[] getUploadFiles(String name); +> + +## 文件上传(Upload) + +WebMVC模块针对文件的上传处理以及对上传的文件操作都非常的简单,通过注解就轻松搞定: + +- @FileUpload:声明控制器方法需要处理上传的文件流; + + > 无参数,需要注意的是文件上传处理的表单enctype属性: + + >
+ > ...... + >
+ +- IUploadFileWrapper:上传文件包装器接口,提供对已上传文件操作的一系列方法; + +示例代码: + + @Controller + @RequestMapping("/demo) + public class UploadController { + + // 处理单文件上传 + @RequestMapping(value = "/upload", method = Type.HttpMethod.POST) + @FileUpload + public IView doUpload(@RequestParam + IUploadFileWrapper file) throws Exception { + // 获取文件名称 + file.getName(); + + // 获取文件大小 + file.getSize(); + + // 获取完整的文件名及路径 + file.getPath(); + + // 获取文件Content-Type + file.getContentType(); + + // 转移文件 + file.transferTo(new File("/temp", file.getName())); + + // 保存文件 + file.writeTo(new File("/temp", file.getName()); + + // 删除文件 + file.delete(); + + // 获取文件输入流对象 + file.getInputStream(); + + // 获取文件输出流对象 + file.getOutputStream(); + + return View.nullView(); + } + + // 处理多文件上传 + @RequestMapping(value = "/uploads", method = Type.HttpMethod.POST) + @FileUpload + public IView doUpload(@RequestParam + IUploadFileWrapper[] files) throws Exception { + + // ...... + + return View.nullView(); + } + } + +文件上传相关配置参数: + + #------------------------------------- + # 文件上传配置参数 + #------------------------------------- + + # 文件上传临时目录,为空则默认使用:System.getProperty("java.io.tmpdir") + ymp.configs.webmvc.upload_temp_dir= + + # 上传文件大小最大值(字节),默认值:10485760(注:10485760 = 10M) + ymp.configs.webmvc.upload_file_size_max= + + # 上传文件总量大小最大值(字节), 默认值:10485760(注:10485760 = 10M) + ymp.configs.webmvc.upload_total_size_max= + + # 内存缓冲区的大小,默认值: 10240字节(=10K),即如果文件大于10K,将使用临时文件缓存上传文件 + ymp.configs.webmvc.upload_size_threshold= + + # 文件上传状态监听器,可选参数,默认值为空 + ymp.configs.webmvc.upload_file_listener_class= + +文件上传状态监听器(upload\_file\_listener\_class)配置: + +WebMVC模块的文件上传是基于Apache Commons FileUpload组件实现的,所以通过其自身提供的ProgressListener接口即可实现对文件上传状态的监听; + +示例代码:实现上传文件的进度计算; + + public class UploadProgressListener implements ProgressListener { + + public void update(long pBytesRead, long pContentLength, int pItems) { + if (pContentLength == 0) { + return; + } + // 计算上传进度百分比 + double percent = (double) pBytesRead / (double) pContentLength; + // 将百分比存储在用户会话中 + WebContext.getContext().getSession().put("upload_progress", percent); + } + } + +> - 将该接口实现类配置到 ymp.configs.webmvc.upload\_file\_listener\_class 参数中; +> +> - 通过Ajax定时轮循的方式获取会话中的进度值,并展示在页面中; + +## 视图(View) + +WebMVC模块支持多种视图技术,包括JSP、Freemarker、Velocity、Text、HTML、JSON、Binary、Forward、Redirect、HttpStatus、Beetl等,也可以通过IView接口扩展实现自定义视图; + +### 控制器视图的表示方法 +> - 通过返回IView接口类型; +> - 通过字符串表达一种视图类型; +> - 无返回值或返回值为空,将使用当前RequestMapping路径对应的JspView视图; + +### 视图文件路径配置 + +> 控制器视图文件基础路径,必须是以 '/' 开始和结尾,默认值为/WEB-INF/templates/; +> +> ymp.configs.webmvc.base_view_path=/WEB-INF/templates/ + +### 视图对象操作示例 + +> 视图文件可以省略扩展名称,通过IView接口可以直接设置请求参数和内容类型; +> +> // 通过View对象创建视图对象 +> IView _view = View.jspView("/demo/test") +> .addAttribute("attr1", "value") +> .addAttribute("attr2", 2) +> .addHeader("head", "value") +> .setContentType(Type.ContentType.HTML.getContentType()); +> +> // 直接创建视图对象 +> _view = new JspView("/demo/test"); +> +> // 下面三种方式的结果是一样的,使用请求路径对应的视图文件返回 +> _view = View.jspView(); +> _view = JspView.bind(); +> _view = new JspView(); + +### WebMVC模块提供的视图 + +JspView:JSP视图; + +> View.jspView("/demo/test.jsp"); +> // = "jsp:/demo/test" + +FreemarkerView:Freemarker视图; + +> View.freemarkerView("/demo/test.ftl"); +> // = "freemarker:/demo/test" + +VelocityView:Velocity视图; + +> View.velocityView("/demo/test.vm"); +> // = "velocity:/demo/test" + +TextView:文本视图; + +> View.textView("Hi, YMPer!"); +> // = "text:Hi, YMPer!" + +HtmlView:HTML文件内容视图; + +> View.htmlView("

Hi, YMPer!

"); +> // = "html:

Hi, YMPer!

" + +JsonView:JSON视图; + +> // 直接传递对象 +> User _user = new User(); +> user.setId("..."); +> ... +> View.jsonView(_user); +> +> // 传递JSON字符串 +> View.jsonView("{id:\"...\", ...}"); +> // = "json:{id:\"...\", ...}" + +BinaryView:二进制数据流视图; + +> // 下载文件,并重新指定文件名称 +> View.binaryView(new File("/temp/demo.txt")) +> .useAttachment("测试文本.txt"); +> // = "binary:/temp/demo.txt:测试文本.txt" +> +> > 若不指定文件名称,则回应头中将不包含 "attachment;filename=xxx" + +ForwardView:请求转发视图; + +> View.forwardView("/demo/test"); +> // = "forward:/demo/test" + +RedirectView:重定向视图; + +> View.redirectView("/demo/test"); +> // = "redirect:/demo/test" + +HttpStatusView:HTTP状态视图 + +> View.httpStatusView(404); +> // = "http_status:404" +> +> View.httpStatusView(500, "系统忙, 请稍后再试..."); +> // = "http_status:500:系统忙, 请稍后再试..." + +BeeltView:Beetl视图; + +> View.beetlView("/demo/test.btl"); +> // = "beetl:/demo/test" + +NullView:空视图; + +> View.nullView(); + +## 验证(Validation) + +WebMVC模块已集成验证模块,控制器方法可以直接使用验证注解完成参数的有效性验证,详细内容请参阅 [验证(Validation)](https://gitee.com/suninformation/ymate-platform-v2/blob/master/ymate-platform-validation/README.md) 模块文档; + +> **说明**: +> - 控制器的参数验证规则全部通过验证注解进行配置并按顺序执行,由WebMVC框架自动调用完成验证过程,无需手动干预; +> - 参数验证过程将在控制器配置的拦截器执行完毕后执行,也就是说拦截器中获取的请求参数值并未验证过; + +## 缓存(Cache) + +### 集成缓存模块 + +WebMVC模块已集成缓存模块,通过@Cacheable注解即可轻松实现控制器方法的缓存,通过配置缓存模块的scope\_processor\_class参数可以支持APPLICATION和SESSION作用域; + + # 设置缓存作用域处理器 + ymp.configs.cache.scope_processor_class=net.ymate.platform.webmvc.support.WebCacheScopeProcessor + +示例代码:将方法执行结果以会话(SESSION)级别缓存180秒; + + @Controller + @RequestMapping("/demo") + @Cacheable + public class CacheController { + + @RequestMapping("/cache") + @Cacheable(scope = ICaches.Scope.SESSION, timeout = 180) + public IView doCacheable(@RequestParam String content) throws Exception { + // ...... + return View.textView("Content: " + content); + } + } + +> **注意**:基于@Cacheable的方法缓存只是缓存控制器方法返回的结果对象,并不能缓存IView视图的最终执行结果; + +### 自定义缓存处理器 + +WebMVC模块提供了缓存处理器IWebCacheProcessor接口,可以让开发者通过此接口对控制器执行结果进行最终处理,该接口作用于被声明@ResponseCache注解的控制器类和方法上; + +> **说明**: 框架提供IWebCacheProcessor接口默认实现`net.ymate.platform.webmvc.support.WebCacheProcessor`用以缓存视图执行结果, +> 但需要注意的是当使用它时, 请检查web.xml的过滤器`DispatchFilter`中不要配置`INCLUDE`,否则将会产生死循环; + +@ResponseCache注解参数说明: + +> cacheName:缓存名称, 可选参数, 默认值为default; +> +> key:缓存Key, 可选参数, 若未设置则由IWebCacheProcessor接口实现自动生成; +> +> processorClass:自定义视图缓存处理器, 可选参数,若未提供则采用默认IWebCacheProcessor接口参数配置; +> +> scope:缓存作用域, 可选参数,可选值为APPLICATION、SESSION和DEFAULT,默认为DEFAULT; +> +> timeout:缓存数据超时时间, 可选参数,数值必须大于等于0,为0表示默认缓存300秒; +> +> useGZip:是否使用GZIP压缩, 默认值为true + +默认IWebCacheProcessor接口参数配置: + + # 缓存处理器,可选参数 + ymp.configs.webmvc.cache_processor_class=demo.WebCacheProc + +> 框架默认提供了该接口的实现类:`net.ymate.platform.webmvc.support.WebCacheProcessor` +> +> - 基于Cache缓存模块使其对@ResponseCache注解中的Scope.DEFAULT作用域支持Last-Modified等浏览器相关配置,并支持GZIP压缩等特性 + +示例代码: + + package demo; + + import net.ymate.platform.webmvc.*; + import net.ymate.platform.webmvc.view.IView; + + public class WebCacheProc implements IWebCacheProcessor { + public boolean processResponseCache(IWebMvc owner, ResponseCache responseCache, IRequestContext requestContext, IView resultView) throws Exception { + // 这里是对View视图自定义处理逻辑... + // 完整的示例代码请查看net.ymate.platform.webmvc.support.WebCacheProcessor类源码 + return false; + } + } + + @Controller + @RequestMapping("/demo") + public class CacheController { + + @RequestMapping("/cache") + @ResponseCache + public IView doCacheable(@RequestParam String content) throws Exception { + // ...... + return View.textView("Content: " + content); + } + } + +> **说明**:该接口方法返回布尔值,用于通知WebMVC框架是否继续处理控制器视图; + +## 拦截器(Intercept) + +WebMVC模块基于YMPv2.0的新特性,原生支持AOP方法拦截,通过以下注解进行配置: + +> @Before:用于设置一个类或方法的前置拦截器,声明在类上的前置拦截器将被应用到该类所有方法上; + +> @After:用于设置一个类或方的后置拦截器,声明在类上的后置拦截器将被应用到该类所有方法上; + +> @Around:用于同时配置一个类或方法的前置和后置拦截器; + +> @Clean:用于清理类上全部或指定的拦截器,被清理的拦截器将不会被执行; + +> @ContextParam:用于设置上下文参数,主要用于向拦截器传递参数配置; + +> @Ignored:声明一个方法将忽略一切拦截器配置; + +> **说明**: +> - 声明`@Ignored`注解的方法、非公有方法和Object类方法及Object类重载方法将不被拦截器处理。 +> - 使用`@Interceptor`注解声明拦截器类,框架将自动扫描加载并支持IoC依赖注入特性。 + +示例代码: + + // 创建自定义拦截器 + public class UserSessionChecker implements IInterceptor { + public Object intercept(InterceptContext context) throws Exception { + // 判断当前拦截器执行方向 + if (context.getDirection().equals(Direction.BEFORE) + && WebContext.getRequest().getSession(false) == null) { + return View.redirectView("/user/login"); + } + return null; + } + } + + @Controller + @RequestMapping("/user") + @Before(UserSessionChecker.class) + public class Controller { + + @RequestMapping("/center") + public IView userCenter() throws Exception { + // ...... + return View.jspView("/user/center"); + } + + @RequestMapping("/login") + @Clean + public IView userLogin() throws Exception { + return View.jspView("/user/login"); + } + } + +## Cookies操作 + +WebMVC模块针对Cookies这个小甜点提供了一个名为CookieHelper的小工具类,支持Cookie参数的设置、读取和移除操作,同时支持对编码和加密处理,并允许通过配置参数调整Cookie策略; + +### Cookie配置参数 + + #------------------------------------- + # Cookie配置参数 + #------------------------------------- + + # Cookie键前缀,可选参数,默认值为空 + ymp.configs.webmvc.cookie_prefix= + + # Cookie作用域,可选参数,默认值为空 + ymp.configs.webmvc.cookie_domain= + + # Cookie作用路径,可选参数,默认值为'/' + ymp.configs.webmvc.cookie_path= + + # Cookie密钥,可选参数,默认值为空 + ymp.configs.webmvc.cookie_auth_key= + + # Cookie密钥验证是否默认开启, 默认值为false + ymp.configs.webmvc.default_enabled_cookie_auth= + + # Cookie是否默认使用HttpOnly, 默认值为false + ymp.configs.webmvc.default_use_http_only= + +### 示例代码:演示Cookie操作 + + // 创建CookieHelper对象 + CookieHelper _helper = CookieHelper.bind(WebContext.getContext().getOwner()); + + // 设置开启采用密钥加密(将默认开启Base64编码) + _helper.allowUseAuthKey(); + + // 设置开启采用Base64编码(默认支持UrlEncode编码) + _helper.allowUseBase64(); + + // 设置开启使用HttpOnly + _helper.allowUseHttpOnly(); + + // 添加或重设Cookie,过期时间基于Session时效 + _helper.setCookie("current_username", "YMPer"); + + // 添加或重设Cookie,并指定过期时间 + _helper.setCookie("current_username", "YMPer", 1800); + + // 获取Cookie值 + BlurObject _currUsername = _helper.getCookie("current_username"); + + // 获取全部Cookie + Map _cookies = _helper.getCookies(); + + // 移除Cookie + _helper.removeCookie("current_username"); + + // 清理所有的Cookie + _helper.clearCookies(); + +## 国际化(I18N) + +基于YMPv2.0框架I18N支持,整合WebMVC模块并提供了默认II18NEventHandler接口实现,相关配置参数: + + // 指定WebMVC模块的I18N资源管理事件监听处理器 + ymp.i18n_event_handler_class=net.ymate.platform.webmvc.support.I18NWebEventHandler + + # 国际化资源文件存放路径,可选参数,默认值为${root}/i18n/ + ymp.configs.webmvc.i18n_resources_home= + + # 国际化资源文件名称,可选参数,默认值为messages + ymp.configs.webmvc.i18n_resource_name= + + # 国际化语言设置参数名称,可选参数,默认值为_lang + ymp.configs.webmvc.i18n_language_param_name= + +加载当前语言设置的步骤: + +> 1. 通过`webmvc.i18n_language_param_name`加载语言设置参数名称,默认值为:`_lang` +> 2. 尝试加载请求作用域中`_lang`参数值; +> 4. 尝试从Cookies中加载`_lang`参数值; +> 5. 使用系统默认语言设置; + +## 约定模式(Convention Mode) + +**名词解释**:约定优于配置(Convention Over Configuration),也称作按约定编程,是一种软件设计范式,通过命名规则之类的约束来减少程序中的配置,旨在减少软件开发人员需要做决定的数量,获得简单的好处,而又不失灵活性。 + +有些时候我们仅仅是为了能够访问一个视图文件而不得不编写一个控制器方法与之对应,当这种重复性的工作很多时,就变成了灾难,因此,在WebMVC模块中,通过开启约定模式即可支持直接访问`base_view_path`路径下的视图文件,无需编写任何代码; + +WebMVC模块的约定模式默认为关闭状态,需要通过配置参数开启: + + ymp.configs.webmvc.convention_mode=true + +### 访问权限规则配置 + +在约定模式模式下,支持设置不同路径的访问权限,规则是:`-`号代表禁止访问,`+`或无符串代表允许访问,多个路径间用`|`分隔; + +访问权限示例:禁止访问admin目录和index.jsp文件,目录结构如下: + + WEB-INF\ + | + |--templates\ + | | + | +--admin\ + | | + | +--users\ + | | + | +--reports\ + | | + | +--index.jsp + | | + | <...> + +示例参数配置: + + ymp.configs.webmvc.convention_view_paths=admin-|index-|users|reports+ + +### 拦截器规则配置 + +由于在约定模式下,访问视图文件无需控制器,所以无法通过控制器方法添加拦截器配置,因此,WebMVC模块针对约定模式单独提供了拦截器规则配置这一扩展功能,主要是通过@InterceptorRule配合IInterceptorRule接口使用; + +拦截器规则设置默认为关闭状态,需要通过配置参数开启: + + ymp.configs.webmvc.convention_interceptor_mode=true + +拦截规则配置示例: + + @InterceptorRule("/demo") + @Before(WebUserSessionCheck.class) + public class InterceptRuleDemo implements IInterceptorRule { + + @InterceptorRule("/admin/*") + @Before(AdminTypeCheckFilter.class) + public void adminAll() { + } + + @Clean + @InterceptorRule("/admin/login") + public void adminLogin() { + } + + @InterceptorRule("/user/*") + public void userAll() { + } + + @InterceptorRule("/mobile/person/*") + public void mobilePersonAll() { + } + } + +> 说明: +> +> @InterceptorRule:拦截器规则注解; +> +> - 在实现IInterceptorRule接口的类上声明,表示该类为拦截规则配置; +> - 在类方法上声明,表示针对一个具体的请求路径配置规则,与@RequestMapping的作用相似; +> +> 规则配置中支持的注解: +> +> - @Before:约定模式下的拦截器仅支持@Before前置拦截; +> - @Clean:清理上层指定的拦截器; +> - @ContextParam:上下文参数; +> - @ResponseCache:声明控制器方法返回视图对象的执行结果将被缓存; + +> **注意**:配置规则类的方法可以是任意的,方法本身无任何意义,仅是通过方法使用注解; + +### URL伪静态 + +WebMVC模块通过约定模式可以将参数融合在URL中,不再通过`?`传递参数,让URL看上去更好看一些; + +伪静态模式默认为关闭状态,需要通过配置参数开启: + + ymp.configs.webmvc.convention_urlrewrite_mode=true + +> 参数传递规则: +> +> - URL中通过分隔符`_`传递多个请求参数; +> - 通过`UrlParams[index]`方式引用参数值; + +伪静态示例: + + URL原始格式: + http://localhost:8080/user/info/list?type=all&page=2&page_size=15 + + URL伪静态格式: + http://localhost:8080/user/info/list_all_2_15 + +请求参数的引用: + + // 通过EL表达式获取参数 + ${UrlParams[0]}:all + ${UrlParams[1]}:2 + ${UrlParams[2]}:15 + +> **注意**:伪静态参数必须是连续的,UrlParams参数集合存储在Request作用域内; + +### 约定模式完整的配置参数 + + #------------------------------------- + # 约定模式配置参数 + #------------------------------------- + + # 是否开启视图自动渲染(约定优于配置,无需编写控制器代码,直接匹配并执行视图)模式,可选参数,默认值为false + ymp.configs.webmvc.convention_mode= + + # Convention模式开启时视图文件路径(基于base_view_path的相对路径,'-'号代表禁止访问,'+'或无符串代表允许访问),可选参数,默认值为空(即不限制访问路径),多个路径间用'|'分隔 + ymp.configs.webmvc.convention_view_paths= + + # Convention模式开启时是否采用URL伪静态(URL中通过分隔符'_'传递多个请求参数,通过UrlParams[index]方式引用参数值)模式,可选参数,默认值为false + ymp.configs.webmvc.convention_urlrewrite_mode= + + # Convention模式开启时是否采用拦截器规则设置,可选参数,默认值为false + ymp.configs.webmvc.convention_interceptor_mode= + +## 高级特性 + +### 控制器请求处理器 + +在WebMVC模块中除了支持标准Web请求的处理过程,同时也对基于XML和JSON协议格式的请求提供支持,有两种使用场景: + +> 场景一:全局设置,将影响所有的控制器方法; + +通过下面的参数配置,默认为default,可选值为[default|json|xml],也可以是开发者自定义的IRequestProcessor接口实现类名称; + + ymp.configs.webmvc.request_processor_class=default + +> 场景二:针对具体的控制器方法进行设置; + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/sayHi") + @RequestProcessor(JSONRequestProcessor.class) + public IView sayHi(@RequestParam String name, @RequestParam String content) { + return View.textView("Hi, " + name + ", Content: " + content); + } + + @RequestMapping("/sayHello") + @RequestProcessor(XMLRequestProcessor.class) + public IView sayHello(@RequestParam String name, @RequestParam String content) { + return View.textView("Hi, " + name + ", Content: " + content); + } + } + +通过POST方式向`http://localhost:8080/demo/sayHi`发送如下JSON数据: + + { "name" : "YMPer", "content" : "Welcome!" } + +通过POST方式向`http://localhost:8080/demo/sayHello`发送如下XML数据: + + + YMPer + + + +> 以上JSON和XML这两种协议格式的控制器方法,同样支持参数的验证等特性; + +### 控制器执行结果自定义响应处理 + +通过`@ResponseBody`注解可以将控制器方法返回的执行结果对象(`String`或`IView`除外)进行自定义输出,默认将以`JSON`格式输出,可以通过`IResponseBodyProcessor`接口自定义实现输出方式; + +- @ResponseBody注解说明: + + > value:自定义对象输出处理器(即IResponseBodyProcessor接口实现类), 默认为:DefaultResponseBodyProcessor.class; + + > contentType:响应头是否携带Content-Type参数项,默认为:true; + + > keepNull:是否保留空值参数项,默认为:true; + + > quoteField:参数键名是否使有引号标识符,默认为:true; + +- 示例代码: + + @Controller + public class HelloController { + + @RequestMapping("/hello") + @ResponseBody + public DemoBean hello() throws Exception { + DemoBean _result = new DemoBean(); + _result.setName("YMPer"); + _result.setAge(10); + // + return _result; + } + } + + 执行结果: + + {"name":"YMPer","age":10} + +### 异常错误处理器 + +**方式一**:WebMVC模块为开发者提供了一个IWebErrorProcessor接口,允许针对异常、验证结果和约定模式的URL解析逻辑实现自定义扩展,框架提供该接口的默认实现类:`net.ymate.platform.webmvc.impl.DefaultWebErrorProcessor`,若不行任何配置则框架将默认使用它; + +通过配置`ymp.configs.webmvc.error_processor_class`参数进行自定义设置,如下所示: + + ymp.configs.webmvc.error_processor_class=net.ymate.platform.webmvc.impl.DefaultWebErrorProcessor + +示例代码: + + public class WebErrorProcessor implements IWebErrorProcessor { + + /** + * 异常时将执行事件回调 + * + * @param owner 所属YMP框架管理器实例 + * @param e 异常对象 + */ + public void onError(IWebMvc owner, Throwable e) { + // ...你的代码逻辑 + } + + /** + * @param owner 所属YMP框架管理器实例 + * @param results 验证器执行结果集合 + * @return 处理结果数据并返回视图对象,若返回null则由框架默认处理 + */ + public IView onValidation(IWebMvc owner, Map results) { + // ...你的代码逻辑 + return View.nullView(); + } + + /** + * 自定义处理URL请求过程 + * + * @param owner 所属YMP框架管理器实例 + * @param requestContext 请求上下文 + * @return 可用视图对象,若为空则采用系统默认 + * @throws Exception 可能产生的异常 + */ + public IView onConvention(IWebMvc owner, IRequestContext requestContext) throws Exception { + // ...你的代码逻辑 + return View.nullView(); + } + } + +**方式二**:通过`@ResponseErrorProcessor`注解并配合`IResponseErrorProcessor`接口实现控制器类或方法指定自定义异常处理过程; + +示例代码: + + public class DemoRequestProcessor implements IResponseErrorProcessor { + + @Override + public IView processError(IWebMvc owner, Throwable e) { + return TextView.bind("Error: " + e.getMessage); + } + } + + @Controller + @RequestMapping("/demo") + public class DemoController { + + @RequestMapping("/sayHi") + @ResponseErrorProcessor(DemoRequestProcessor.class) + public IView sayHi(@RequestParam String name, @RequestParam String content) { + // 模拟异常 + System.out.println(1 / 0); + return View.textView("Hi, " + name + ", Content: " + content); + } + } + +**方式三** 通过`@ExceptionProcessor`为指定的异常类型设置其默认的错误响应码和描述信息; + +通过`@ExceptionProcessor`声明的类必须是`Throwable`为的子类,框架初始化时将被自动注册,也可以通过手工方式注册,代码如下: + + ExceptionProcessHelper.DEFAULT.registerProcessor(MyException.class, new IExceptionProcessor() { + @Override + public Result process(Throwable target) throws Exception { + return new Result(10010, "Customize exception description."); + } + }); + +- @ExceptionProcessor注解说明: + + > code:异常错误码,非0数字; + > + > msg:默认错误描述 + +当框架使用`net.ymate.platform.webmvc.impl.DefaultWebErrorProcessor`作为异常错误处理器时,它将首先尝试加载对应异常的错误响应配置,其内部处理逻辑代码如下: + + IExceptionProcessor _processor = ExceptionProcessHelper.DEFAULT.bind(e.getClass()); + if (_processor != null) { + IExceptionProcessor.Result _result = _processor.process(_unwrapThrow); + showErrorMsg(_result.getCode(), WebUtils.errorCodeI18n(__owner, _result), null).render(); + } + +如上所示,通过`WebUtils.errorCodeI18n(__owner, _result)`方法调用,实现尝试优先加载`code`响应码对应的国际化资源,国际化资源文件中的配置采用`webmvc.error_code_`方式,如下: + + webmvc.error_code_10010=自定义异常描述 + +> **注意**:相同异常类型不允许重复注册,仅首次注册生效。 + +### 控制器包配置 + +通过`package-info.java`为包中同级的控制器类添加通用配置,允许使用的注解: + +- @RequestMapping:控制器请求路径映射; +- @RequestProcessor:控制器请求自定义处理器注解; +- @ParameterEscape:控制器方法参数转义注解; +- @ResponseCache:控制器方法返回视图对象执行结果缓存注解; +- @ResponseHeader:控制器方法返回结果时增加响应头参数; +- @ResponseView:控制器方法默认返回视图对象; +- @ResponseBody: 控制器方法返回结果对象自定义输出; +- @ResponseErrorProcessor:控制器方法的默认异常处理器; + +示例代码: + +> 本例将为`net.ymate.demo.controller`包指定统一的请求路径映射和响应头配置,其`package-info.java`内容如下: + + @RequestMapping("/v1/api") + @ResponseHeader({ + @Header(name = "X-Request-Token", value = "c43e3bab82ab45278b0d5872d6c6d3b6"), + @Header(name = "X-Request-Scope", value = "request")}) + package net.ymate.demo.controller; + + import net.ymate.platform.webmvc.annotation.Header; + import net.ymate.platform.webmvc.annotation.RequestMapping; + import net.ymate.platform.webmvc.annotation.ResponseHeader; diff --git a/misc/site/quickstart.md b/misc/site/quickstart.md new file mode 100755 index 00000000..cead53df --- /dev/null +++ b/misc/site/quickstart.md @@ -0,0 +1,195 @@ +--- +sidebar: auto +sidebarDepth: 2 +--- + +# 快速上手 + +本教程将介绍如何使用`ymate-maven-extension`扩展工具,快速搭建基于YMP框架的Java工程及如何通过Maven完成编译、运行等一系列操作。 + +## 项目模板 + +> 目前`ymate-maven-extension`扩展工具支持以下4种项目模板: + +- `ymate-archetype-quickstart (quickstart)`:标准Java工程,已集成YMP依赖; + +- `ymate-archetype-webapp (webapp)`:JavaWeb工程,已集成WebMVC框架相关依赖和完整的参数配置; + +- `ymate-archetype-module (module)`:YMP模块工程,提供Demo示例及JUint测试代码; + +- `ymate-archetype-serv (serv)`:YMP服务工程,分别提供TCP、UDP客户端和服务端示例程序及相关配置; + +## 下载安装扩展工具 + +- 步骤1:下载扩展工具源码 + + 执行命令: + + ``` + git clone https://gitee.com/suninformation/ymate-maven-extension.git + ``` + +- 步骤2:编译并安装到本地Maven仓库 + + 执行命令: + + ``` + cd ymate-maven-extension + mvn clean install + ``` + +## 开始搭建工程 + +- 步骤1:开启本地`archetype`向导 + + 执行命令: + + ``` + mvn archetype:generate -DarchetypeCatalog=local + ``` + + 屏幕输出: + + ``` + Choose archetype: + 1: local -> net.ymate.maven.archetypes:ymate-archetype-module (module) + 2: local -> net.ymate.maven.archetypes:ymate-archetype-quickstart (quickstart) + 3: local -> net.ymate.maven.archetypes:ymate-archetype-serv (serv) + 4: local -> net.ymate.maven.archetypes:ymate-archetype-webapp (webapp) + Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : + ``` + + > 注:若执行命令没有显示上述内容,请执行`mvn archetype:crawl`命令后再试! + +- 步骤2:根据实际需求选择项目模板类型,这里我选择:4 + + 屏幕输出,接下来要按屏幕提示进行设置: + + ``` + Define value for property 'groupId': : net.ymate.platform.examples + Define value for property 'artifactId': : ymp-examples-webapp + Define value for property 'version': 1.0-SNAPSHOT: : + Define value for property 'package': net.ymate.platform.examples: : + Confirm properties configuration: + groupId: net.ymate.platform.examples + artifactId: ymp-examples-webapp + version: 1.0-SNAPSHOT + package: net.ymate.platform.examples + Y: : + ``` + + 回车键确认后,开始生成工程结构: + + ``` + [INFO] ---------------------------------------------------------------------------- + [INFO] Using following parameters for creating project from Archetype: ymate-archetype-webapp:1.0-SNAPSHOT + [INFO] ---------------------------------------------------------------------------- + [INFO] Parameter: groupId, Value: net.ymate.platform.examples + [INFO] Parameter: artifactId, Value: ymp-examples-webapp + [INFO] Parameter: version, Value: 1.0-SNAPSHOT + [INFO] Parameter: package, Value: net.ymate.platform.examples + [INFO] Parameter: packageInPathFormat, Value: net/ymate/platform/examples + [INFO] Parameter: package, Value: net.ymate.platform.examples + [INFO] Parameter: version, Value: 1.0-SNAPSHOT + [INFO] Parameter: groupId, Value: net.ymate.platform.examples + [INFO] Parameter: artifactId, Value: ymp-examples-webapp + [INFO] project created from Archetype in dir: /Users/suninformation/Temp/ymp-examples-webapp + [INFO] ------------------------------------------------------------------------ + [INFO] BUILD SUCCESS + [INFO] ------------------------------------------------------------------------ + [INFO] Total time: 1:08.723s + [INFO] Finished at: Thu Mar 17 11:00:54 CST 2016 + [INFO] Final Memory: 13M/155M + [INFO] ------------------------------------------------------------------------ + ``` + + 至此,基于扩展工具快速搭建YMP工程已完成! + +## 编译并运行 + +首先,进入新创建的工程目录中,执行如下命令: + + cd ymp-examples-webapp + +然后,通过Maven进行代码编译并打包,执行如下命令: + + mvn clean compile package + +屏幕输出: + + Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 + [INFO] Scanning for projects... + [INFO] + [INFO] ------------------------------------------------------------------------ + [INFO] Building ymp-examples-webapp 1.0-SNAPSHOT + [INFO] ------------------------------------------------------------------------ + [INFO] ......(此处省略10000字) + [INFO] ------------------------------------------------------------------------ + [INFO] BUILD SUCCESS + [INFO] ------------------------------------------------------------------------ + [INFO] Total time: 2.582s + [INFO] Finished at: Thu Mar 17 11:43:19 CST 2016 + [INFO] Final Memory: 17M/212M + [INFO] ------------------------------------------------------------------------ + +最后,通过Maven启动Tomcat服务并运行war包,执行如下命令: + + mvn tomcat:run-war + +屏幕输出: + + Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 + [INFO] Scanning for projects... + [INFO] + [INFO] ------------------------------------------------------------------------ + [INFO] Building ymp-examples-webapp 1.0-SNAPSHOT + [INFO] ------------------------------------------------------------------------ + [INFO] + [INFO] ......(此处省略10000字) + [INFO] + [INFO] <<< tomcat-maven-plugin:1.1:run-war (default-cli) @ ymp-examples-webapp <<< + [INFO] + [INFO] --- tomcat-maven-plugin:1.1:run-war (default-cli) @ ymp-examples-webapp --- + [INFO] Running war on http://localhost:8080/ymp-examples-webapp + [INFO] Creating Tomcat server configuration at /Users/suninformation/Temp/ymp-examples-webapp/target/tomcat + 三月 17, 2016 11:48:31 上午 org.apache.catalina.startup.Embedded start + 信息: Starting tomcat server + 三月 17, 2016 11:48:32 上午 org.apache.catalina.core.StandardEngine start + 信息: Starting Servlet Engine: Apache Tomcat/6.0.29 + [INFO] YMP - + __ ____ __ ____ ____ + \ \ / / \/ | _ \ __ _|___ \ + \ V /| |\/| | |_) | \ \ / / __) | + | | | | | | __/ \ V / / __/ + |_| |_| |_|_| \_/ |_____| Website: http://www.ymate.net/ + [INFO] YMP - Initializing ymate-platform-core-2.0.0-GA build-20160315-0206 - debug:true + [INFO] Validations - Initializing ymate-platform-validation-2.0.0-GA build-20160315-0206 + [INFO] WebMVC - Initializing ymate-platform-webmvc-2.0.0-GA build-20160315-0206 + [INFO] Caches - Initializing ymate-platform-cache-2.0.0-GA build-20160315-0206 + [INFO] Logs - Initializing ymate-platform-log-2.0.0-GA build-20160315-0206 + [INFO] Cfgs - Initializing ymate-platform-configuration-2.0.0-GA build-20160315-0206 + [INFO] ......(此处省略10000字) + [INFO] YMP - Initialization completed, Total time: 839ms + 三月 17, 2016 11:48:33 上午 org.apache.coyote.http11.Http11Protocol init + 信息: Initializing Coyote HTTP/1.1 on http-8080 + 三月 17, 2016 11:48:33 上午 org.apache.coyote.http11.Http11Protocol start + 信息: Starting Coyote HTTP/1.1 on http-8080 + +看到上面输出信息,说明Tomcat服务已启动,并已成功运行war包。 + +请打开浏览器访问:[http://localhost:8080/ymp-examples-webapp/](http://localhost:8080/ymp-examples-webapp/) + +浏览器输出内容: + + Hello YMP world! + +**Congratulations!** 恭喜你成功使用`Maven`完成了对`YMP`项目的创建、编译、运行等一系列操作,亲自动手尝试一下吧! + +::: tip One More Thing + +`YMP`不仅提供便捷的`Web`及其它`Java`项目的快速开发体验,也将不断提供更多丰富的项目实践经验。 + +感兴趣的小伙伴儿们可以加入 官方`QQ`群`480374360`,一起交流学习,帮助`YMP`成长! + +了解更多有关`YMP`框架的内容,请访问官网:[https://ymate.net](https://ymate.net) +::: \ No newline at end of file -- Gitee