# rpcgen
**Repository Path**: l0km/rpcgen
## Basic Information
- **Project Name**: rpcgen
- **Description**: 基于codegen,sql2java,spring-boot快速搭建微服务项目框架
- **Primary Language**: Unknown
- **License**: BSD-2-Clause
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 2
- **Created**: 2021-11-27
- **Last Updated**: 2024-12-17
## Categories & Tags
**Categories**: Uncategorized
**Tags**: 自动化工具
## README
# rpcgen
[TOC]
## 概述
rpcgen是一个项目模板生成工具,将基于[spring-boot](https://spring.io/projects/spring-boot/),[codegen](https://gitee.com/l0km/codegen),[sql2java](https://gitee.com/l0km/sql2java)实现的Java Web API服务项目代码模板通过脚本生成初始的代码框架,帮助开发者快速搭建一个Java Web API服务。
设计一个简单的Java Web API服务,有一些共通的地方:
- 一般都需要数据库支持(比如mysql),需要对数据库进行增删改查操作
- 需要提供RESTful风格的HTTP协议服务端口以响应前端的请求
- 需要提供在线文档(Swagger),以供前端开发人员了解接口定义和在线测试
- 需要简单的命令行脚本编译生成Fat-Jar,以支持快速无依赖的部署
- 需要shell脚本支持以Linux Service方式管理服务的启动和停止
- 需要支持参数配置文件,以方便在运维过程中对服务的运行参数进行修改和定义
- 在HTTP服务之外可能还需要提供RPC方式的client(Java/C++)用于Java和C++程序的服务访问请求
rpcgen设计的目的就是将上面这些共通的需求都集成起来由这个服务框架提供初始实现,帮助开发者快速搭建一个服务框架,使用代码生成工具生成重复逻辑代码,帮助开发者减少手工代码设计的工作量,以提高代码质量,将主要精力聚焦在实现业务逻辑。
## rpcgen 能做什么
- 根据开发者提供的建表语句自动生成所有基本的数据库表操作代码,开发者大部分情况下不再需要自己通过SQL语言实现查询.
- 提供JVM层的数据库表缓存机制对主键/唯一键提供缓存机制,增加数据库查询效率
- 允许开发者自定义模板(velocity)生成数据库操作方法
- 自动生成RESTful风格的HTTP接口服务代码
- 自动根据类和方法注释生成服务接口的Swagger在线文档----只要写好服务接口的代码注释,在线文档自动生成。
- 自动生成基于thrift的RPC Java client 代码(支持android)
- 自动生成基于thrift的RPC C++11 client 代码(支持跨平台编译)
- 提供用户自定义配置文件读写机制,允许开发者设计用户配置参数
- 提供完善的命令行编译打包脚本(bat/bash),启动脚本,调试启动脚本(以支持远程调试)。
- 提供发行包创建脚本,服务部署脚本,这些简单而重复的工作都可以通过执行对应的脚本自动完成。
## rpcgen文件结构
```
├── images
├── LICENSE
├── prjgen.sh ## 初始项目生成脚本程序
├── README.md ## 使用说明文档
├── rpcgen_status.init ## prjgen.sh 初始参数
└── template ## Java Web API项目代码模板
```
## 快速开始
### hello-world
`prjgen.sh`是一个shell(bash) 脚本程序,它可以根据用户输入的参数(项目名称,包名...)完成对项目代码模板[template]()中的所有源码文件进行重构生成初始的项目框架。
`prjgen.sh` 可以在Linux或MacOS平台运行,也可以在Windows/MSYS(如git bash)下运行
运行环境要求:
- maven
prjgen.sh 生成完代码后需要调用Maven对生成的代码进行编译,以验证生成代码的正确性,所以要求你的系统已经正确安装了Maven(3.5.0 or above)
- git
生成的项目在Maven编译时,`buildnumber`插件会调用git命令获取项目的版本号,所以还需要系统安装了git
请执行 [`prjgen.sh`](),根据提示生成初始的项目代码,执行成功最后会输出如下提示(假定生成的项目名称叫`hello-world`)
```bash
rpcgen $ ./prjgen.sh
```

这一步正常完成时,`prjgen.sh` 已经根据你输入的参数在`target`文件夹下生成了一个以你指定的项目名称命名的包含完整Java服务初始项目源码的文件夹,它已经是可编译可运行的Java服务了。
> `prjgen.sh` 运行时会生成 `rpcgen_status`文件用于保存用户输入的所有参数,如果`rpcgen_status`不存在会自动从`rpcgen_status.init`复制一份
>
> 所以如果你想清除所有上次执行`prjgen.sh`时输入的数据重新生成项目,直接删除 `rpcgen_status` 文件就可以了(**不要删除`rpcgen_status.init`**)
### hello-world服务预览
`prjgen.sh`执行成功后,你肯定想先看看这个生成的hello-world服务长啥样儿。
#### Maven编译打包
命令行执行`shade-package`脚本就可以完成Maven编译打包全过程。
> 显然,要成功执行`shade-package`脚本,你必须已经有安装Maven (3.5.0 or above)。
Windows CMD
```shell
> cd target\hello-world
> shade-package.bat
```
Linux Shell
```bash
$ cd target/hello-world
$ ./shade-package.sh
```
编译完成`target/hello-world/hello-world-service/target`下生成包含所有依赖库的Far-Jar:`hello-world-service-0.0.0-SNAPSHOT-standalone.jar`
```
J:\rpcgen\target\hello-world\hello-world-service\target 的目录
2021/12/07 23:56
.
2021/12/07 23:56 ..
2021/12/07 23:06 classes
2021/12/07 22:15 generated-sources
2021/12/07 23:55 generated-test-sources
2021/12/07 23:56 34,827,500 hello-world-service-0.0.0-SNAPSHOT-standalone.jar
2021/12/07 23:55 57,206 hello-world-service-0.0.0-SNAPSHOT.jar
2021/12/07 23:55 maven-archiver
2021/12/07 22:15 maven-status
2021/12/07 23:55 123 start_hello_world_server.bat
2021/12/07 23:55 213 start_hello_world_server.sh
2021/12/07 23:55 148 start_hello_world_server_debug.bat
2021/12/07 23:55 180 start_hello_world_server_debug.sh
2021/12/07 23:06 test-classes
6 个文件 34,885,370 字节
8 个目录 20,319,440,896 可用字节
```
#### 启动服务
执行`start_hello_world_server.bat/sh`就可以启动hello-world服务了。
```
J:\rpcgen\target\hello-world>hello-world-service\start_hello_world_server.bat
ooo. .oo. .oo. oooo ooo oooo d8b oo.ooooo. .ooooo.
`888P"Y88bP"Y88b `88. .8' `888""8P 888' `88b d88' `"Y8
888 888 888 `88..8' 888 888 888 888
888 888 888 `888' 888 888 888 888 .o8
o888o o888o o888o .8' d888b 888bod8P' `Y8bod8P'
.o..P' 888
`Y8P' o888o
[main][INFO ] (FluentPropertyBeanIntrospector.java:147) Error when creating PropertyDescriptor for public final void org.apache.commons.configuration2.AbstractConfiguration.setProperty(java.lang.String,java.lang.Object)! Ignoring this property.
[main][INFO ] (FluentPropertyBeanIntrospector.java:147) Error when creating PropertyDescriptor for public final void org.apache.commons.configuration2.AbstractConfiguration.setProperty(java.lang.String,java.lang.Object)! Ignoring this property.
[main][INFO ] (SyslogConfig.java:54) syslog.level(系统日志级别 [OFF,FATAL,ERROR,WARN,INFO,DEBUG,ALL]) = INFO
[main][INFO ] (SyslogConfig.java:61) syslog.location(系统日志文件位置) = log/hello_world.log
[main] (ConfigUtils.java:173) Load properties from jar:file:/J:/rpcgen/target/hello-world/hello-world-service/target/hello-world-service-0.0.0-SNAPSHOT-standalone.jar!/BOOT-INF/lib/hello-world-db-0.0.0-SNAPSHOT.jar!/conf/hello_world_database.properties
[main][INFO ] (ThriftServerService.java:276) RPC Service Parameters(服务运行参数):
[main][INFO ] (ThriftServerService.java:277) port: 28081
[main][INFO ] (ThriftServerService.java:278) connectionLimit: 32
[main][INFO ] (ThriftServerService.java:279) workerThreads: 8
[main][INFO ] (ThriftServerService.java:280) idleConnectionTimeout: 60.00s
[main][INFO ] (ThriftServerService.java:281) taskExpirationTimeout: 5.00s
[IHelloWorld(T:framed,P:binary) STARTING][INFO ] (NettyServerTransport.java:155) started transport thrift:28081 (:28081)
[IHelloWorld(T:framed,P:binary) STARTING][INFO ] (ThriftServerService.java:267) IHelloWorld(T:framed,P:binary) service is running(服务启动)
[main][INFO ] (ThriftServerService.java:276) RPC Service Parameters(服务运行参数):
[IHelloWorld(T:http,P:json) STARTING][INFO ] (NettyServerTransport.java:155) started transport thrift:28082 (:28082)
[main][INFO ] (ThriftServerService.java:277) port: 28082
[IHelloWorld(T:http,P:json) STARTING][INFO ] (ThriftServerService.java:267) IHelloWorld(T:http,P:json) service is running(服务启动)
[main][INFO ] (ThriftServerService.java:278) connectionLimit: 32
[main][INFO ] (ThriftServerService.java:279) workerThreads: 8
[main][INFO ] (ThriftServerService.java:280) idleConnectionTimeout: 100.00ms
[main][INFO ] (ThriftServerService.java:281) taskExpirationTimeout: 5.00s
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.9.RELEASE)
[main][INFO ] (StartupInfoLogger.java:48) Starting HelloWorldServiceMain on DESKTOP-U2UNNEO with PID 10048 (J:\rpcgen\target\hello-world\hello-world-service\target\hello-world-service-0.0.0-SNAPSHOT-standalone.jar started by 10km in J:\rpcgen\target\hello-world\hello-world-service\target)
[main][INFO ] (SpringApplication.java:593) No active profile set, falling back to default profiles: default
[main][INFO ] (AbstractApplicationContext.java:583) Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6442b0a6: startup date [Tue Dec 07 23:59:15 CST 2021]; root of context hierarchy
[background-preinit][INFO ] (Version.java:30) HV000001: Hibernate Validator 5.3.6.Final
[main][INFO ] (AutowiredAnnotationBeanPostProcessor.java:155) JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[main][INFO ] (TomcatEmbeddedServletContainer.java:89) Tomcat initialized with port(s): 8080 (http)
十二月 07, 2021 11:59:18 下午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
十二月 07, 2021 11:59:18 下午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet Engine: Apache Tomcat/8.5.23
十二月 07, 2021 11:59:18 下午 org.apache.catalina.core.ApplicationContext log
信息: Initializing Spring embedded WebApplicationContext
[localhost-startStop-1][INFO ] (EmbeddedWebApplicationContext.java:276) Root WebApplicationContext: initialization completed in 2239 ms
[localhost-startStop-1][INFO ] (ServletRegistrationBean.java:190) Mapping servlet: 'dispatcherServlet' to [/]
[main][INFO ] (PropertySourcedRequestMappingHandlerMapping.java:69) Mapped URL path [/v2/api-docs] onto method [public org.springframework.http.ResponseEntity springfox.documentation.swagger2.web.Swagger2Controller.getDocumentation(java.lang.String,javax.servlet.http.HttpServletRequest)]
[main][INFO ] (RequestMappingHandlerAdapter.java:534) Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6442b0a6: startup date [Tue Dec 07 23:59:15 CST 2021]; root of context hierarchy
[main][INFO ] (AbstractUrlHandlerMapping.java:362) Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
[main][INFO ] (AbstractUrlHandlerMapping.java:362) Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
[main][INFO ] (AbstractUrlHandlerMapping.java:362) Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
[main][INFO ] (MBeanExporter.java:431) Registering beans for JMX exposure on startup
[main][INFO ] (DefaultLifecycleProcessor.java:343) Starting beans in phase 2147483647
[main][INFO ] (DocumentationPluginsBootstrapper.java:147) Context refreshed
[main][INFO ] (DocumentationPluginsBootstrapper.java:150) Found 1 custom documentation plugin(s)
[main][INFO ] (ApiListingReferenceScanner.java:41) Scanning for api listing references
[main][INFO ] (TomcatEmbeddedServletContainer.java:201) Tomcat started on port(s): 8080 (http)
[main][INFO ] (StartupInfoLogger.java:57) Started HelloWorldServiceMain in 4.607 seconds (JVM running for 6.728)
PRESS 'quit' OR 'CTRL-C' to exit
```
> 最后一行提示,输入`quit`或按`CTRL-C`组合键就可以退出服务
#### Swagger
服务正常启动后,访问 http://127.0.0.1:8080/swagger-ui.html 就可以看到hello-world的Swagger在线文档了

### 项目文件结构
生成的`target/hello-world`下的项目文件结构如下(服务接口类为`IHelloWorld`):
```
└── hello-world ## 项目代码模板
├── clean_mprj.sh ## 清除所有Maven或IDE生成的文件的脚本
├── hello-world-base ## 服务接口定义模块
│ └── src
│ ├── codegen #### [自动生成]服务接口的HTTP协议服务
│ │ #### 对应 SpringController(控制器)
│ └── main #### 服务接口(IHelloWorld)定义,公用常量定义
├── hello-world-client #### [自动生成]Java Client(通用平台)模块
│ └── src
│ ├── codegen #### [自动生成] codegen插件基于Thrift Client Stub
│ │ #### 生成的Java Client(通用平台)模块
│ └── thrift #### [自动生成] (swift插件)基于服务接口定义文件(IDL)
│ #### 生成的Thrift Client Stub 代码
├── hello-world-client-thrifty ## [自动生成]Java Client(支持android)模块
│ └── src
│ ├── codegen #### [自动生成] codegen插件基于Microsoft/Thrifty Client Stub
│ │ #### 生成的Java Client(支持Android平台)模块
│ └── thrifty #### [自动生成] thrifty-compiler插件基于服务接口定义文件(IDL)
│ #### 生成的Microsoft/Thrifty Client Stub 代码
├── hello-world-client-base ## Java Client公用模块
├── hello-world-client-cpp ## [自动生成]C++ Client(支持跨平台编译)模块
│ ├── CMakeLists.txt #### cmake工程脚本
│ ├── conanfile.py #### conan 配置脚本
│ ├── gen-client-cpp.bat #### thrift compiler 生成stub代码脚本
│ ├── gen-client-cpp.sh #### thrift compiler 生成stub代码脚本
│ ├── gen-decorator-client-cpp.bat #### codegen生成decorator代码脚本
│ ├── gen-decorator-client-cpp.sh #### codegen生成decorator代码脚本
│ ├── gen.bat #### 生成所有client C++ 代码的脚本
│ ├── gen.sh #### 生成所有client C++ 代码的脚本
│ ├── gnu_build.sh #### GCC编译脚本
│ ├── make_mingw_project.bat #### 生成MinGW 编译的Makefile工程
│ ├── make_msvc_project.bat #### Visual Studio编译脚本
│ └─src #### [自动生成] C++ client代码
│ └── client #### decorator 代码
│ └── stub #### thrift stub 代码
├── hello-world-db ## [自动生成]数据库操作模块
│ ├── create_table.bat #### 创建数据库表脚本(Windows CMD)
│ ├── create_table.sh #### 创建数据库表脚本(bash shell)
│ ├── gen-mysql.bat #### 生成数据库操作代码脚本(Windows CMD)
│ ├── gen-mysql.properties #### sql2java配置文件
│ ├── gen-mysql.sh #### 生成数据库操作代码脚本(bash shell)
│ ├── sql
│ │ └── create_table.sql #### 表结构定义SQL脚本
│ └── src
│ └── main #### [自动生成] sql2java插件根据gen-mysql.properties
│ #### 生成的数据操作代码
├── hello-world-local ## 服务接口实现模块
│ ├── src
│ │ ├── main #### 服务接口实现(HelloWorldImpl)
│ │ └── sql2java #### [自动生成] sql2java插件根据vm/custom中的velocity模板
│ │ #### 生成的数据库基础访问对象(BasDao)代码
│ └── vm
│ └── custom #### 自定义模板(velocity):数据库基础访问对象(BasDao)模板
├── hello-world-service ## 服务启动模块
│ ├── dist.sh #### 服务发行包生成脚本
│ ├── gen.bat #### 代码生成脚本
│ ├── gen_client.bat #### 代码生成脚本
│ ├── gen_client.sh #### 代码生成脚本
│ ├── gen_client-thrifty.bat #### 代码生成脚本
│ ├── gen_client-thrifty.sh #### 代码生成脚本
│ ├── gen-decorator-client.bat #### 代码生成脚本
│ ├── gen-decorator-client.sh #### 代码生成脚本
│ ├── gen-decorator-client-thrifty.bat #### 代码生成脚本
│ ├── gen-decorator-client-thrifty.sh #### 代码生成脚本
│ ├── gen-decorator-service.bat #### 代码生成脚本
│ ├── gen-decorator-service.sh #### 代码生成脚本
│ ├── gen.sh #### 代码生成脚本
│ ├── gen_thrift.bat #### 服务接口定义文件(IDL)生成脚本(Windows CMD)
│ ├── gen_thrift.sh #### 服务接口定义文件(IDL)生成脚本(bash shell)
│ ├── hello_world #### Linux服务启动脚本
│ ├── hello_world.service.in #### Linux(systemctl)服务配置模板
│ ├── IHelloWorld.thrift #### (swift2thrift插件)基于服务接口生成的服务接口定义文件(IDL)
│ ├── install.sh #### Linux(systemctl)服务安装脚本
│ ├── src
│ │ ├── codegen #### [自动生成] codegen插件基于服务接口生成的服务接口实例装饰类
│ │ └── main #### 服务启动实现代码
│ ├── start_hello_world_server.bat #### 命令启动服务脚本(Windows CMD)
│ ├── start_hello_world_server_debug.bat #### 命令(调试)启动服务脚本(Windows CMD)
│ ├── start_hello_world_server_debug.sh #### 命令(调试)启动服务脚本(bash shell)
│ └── start_hello_world_server.sh #### 命令启动服务脚本(bash shell)
├── shade-package.bat ## Fat-Jar打包命令(Windows CMD)
└── shade-package.sh ## Fat-Jar打包命令(bash shell)
```
## 数据库设计
生成项目框架只是第一步,接下来,就要设计与具体业务相关的数据库表结构设计了。
**下面的章节我们假定生成的项目名称叫`hello-world`,那么在`target/hello-world/hello-world-db`(对应[template/hello-world-db](template/hello-world-db))下是`hello-world`项目的数据库操作相关的代码。后续所有的描述如不特别说明,所说的项目都假设为生成的`target/hello-world`项目。**
### 表结构定义
当你用`prjgen.sh`生成了自己的项目后,请在`sql/create_table.sql`这个初始脚本基础上开始你自己的数据库表结构设计。参见 [项目文件结构](### 项目文件结构)
`sql/create_table.sql`为预定义的数据库建表语句SQL脚本(mysql语法),只是为了示例需要创建了一个名为`my_person`的`人员基本描述信息`表,你可以完全删除`my_person`重新开始。
下面的例子中,我们在`sql/create_table.sql`基础上增加了一张设备表`my_device`。
```sql
set character set utf8;
SET NAMES 'utf8';
############################
# delete all table/view ###
############################
DROP TABLE IF EXISTS my_device ;
DROP TABLE IF EXISTS my_person ;
############################
# create all table/view ###
############################
# 所有表中
# create_time 记录创建时间戳 (默认提供数据库服务器时间)
# update_time 记录创建时间戳 (默认提供数据库服务器时间)
CREATE TABLE IF NOT EXISTS my_person (
`id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户id',
`name` varchar(32) NOT NULL COMMENT '姓名',
`sex` tinyint(1) DEFAULT NULL COMMENT '性别,0:女,1:男,其他:未定义',
`password` char(32) DEFAULT NULL COMMENT '用户密码,MD5',
`birthdate` date DEFAULT NULL COMMENT '出生日期',
`mobile_phone`char(11) DEFAULT NULL UNIQUE COMMENT '手机号码',
`papers_type` tinyint(1) DEFAULT NULL COMMENT '证件类型,0:未知,1:身份证,2:护照,3:台胞证,4:港澳通行证,5:军官证,6:外国人居留证,7:员工卡,8:其他',
`papers_num` varchar(32) DEFAULT NULL UNIQUE COMMENT '证件号码' ,
`remark` varchar(256) DEFAULT NULL COMMENT '备注',
`create_time` timestamp DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
# 验证 papers_type 字段有效性
CHECK(papers_type>=0 AND papers_type<=8),
# 验证 sex 字段有效性
CHECK(sex>=0 AND sex<=1)
# 验证 rank 字段有效性
) COMMENT '人员基本描述信息' DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS my_device (
`id` varchar(128) NOT NULL PRIMARY KEY COMMENT '设备id,唯一识别设备的识别码',
`physical_address` varchar(32) NOT NULL COMMENT '设备物理地址,MAC地址,IMEI或其他设备识别码(不允许重复)',
`address_type` varchar(16) NOT NULL DEFAULT 'MAC' COMMENT '设备物理地址,默认6字节MAC地址(HEX)',
`owner_id` int(11) DEFAULT NULL COMMENT '所属用户id',
`device_name` varchar(32) DEFAULT NULL COMMENT '设备名称',
`model` varchar(32) DEFAULT NULL COMMENT '设备型号',
`remark` varchar(256) DEFAULT NULL COMMENT '备注',
`create_time` timestamp DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `physical_address` (`physical_address` ASC),
INDEX `owner_id` (`owner_id` ASC),
FOREIGN KEY (owner_id) REFERENCES my_person(id) ON DELETE CASCADE
) COMMENT '设备基本信息' DEFAULT CHARSET=utf8;
```
#### 建表语句的顺序
如上示例为确保建表语句成功执行,会先删除已经存在的表,所以脚本中前半部分为`DROP TABLE`删表语句,后半部分才是`CREATE TABLE`。
因为表之间可能会有外键引用关系(如上示例中`my_device.owner_id`就是外键引用了`my_person.id`字段),所以当有多张表/视图的时候,创建表/视图的顺序必须与删除表/视图的顺序相反,也就是先建的表/视图最后删除,这样才能确保SQL脚本能正确执行。
#### 表和字段命名规则
在设计表结构时,表和字段命名要求使用蛇形命名法(snake-case),即全小写字母+数字,单词之间用`'_'`作连接符,如上面SQL脚本中的示例。后续代码生成工具(sql2java)在生成数据库表操作的Java代码中会将蛇形命名法(snake-case)的符号转为驼峰命名法(camel-case)符号。
- 表名转为大驼峰命名法的类名
- 字段名转为小驼峰命名法的成员变量名
- 表名和字段名在常量定义中都转为大写
| 表名/字段名 | Java定义类型 | Java类名/成员变量 |
| ------------ | ------------- | ----------------------------- |
| TABLE my_person | 表记录类名(Java Bean) | PersonBean |
| | 表操作类名(Interface) | IPersonManager |
| | 表元数据类名 | PersonMetaData |
| | 常量定义 | MY_PERSON,参见生成的Constant.java |
| COLUMN mobile_phone | 成员变量 | mobilePhone |
| | getter/setter方法名 | getMobilePhone/setMobilePhone |
| | 常量定义 | MOBILE_PHONE,参见生成的Constant.java |
| COLUMN name | 成员变量 | name |
| | getter/setter方法名 | getName/setName |
| | 常量定义 | NAME,参见生成的Constant.java |
#### 表名前缀
对于表名,要有一个统一的前缀,在上面的示例中`my_person,my_device`有一个相同的前缀`my_`,这个前缀用于在数据库中区分所属的不同应用,代码生成工具(sql2java)在生成数据库表操作的Java代码时,表名的前缀在转换成类名时会被忽略。
#### SQL注释
SQL的注释很重要
建议如示例脚本中的一样对创建的表和字段都加上注释字段(`COMMENT`),表和字段有了注释字段,后续生成的数据库操作代码时,这些注释内容会自动被添加到生成的代码中,如下是模板文件夹根据`my_person`表生成的[PersonBean](template/myrpc-db/src/main/java/myorg/myrpc/db/PersonBean.java)类的代码片段,可以看到表和每个字段的注释都是来自表结构定义中对应的`COMMENT`字段的内容:
```java
/**
* PersonBean is a mapping of my_person Table.
*
Meta Data Information (in progress):
*
* @author guyadong
*/
@ThriftStruct
@ApiModel(description="人员基本描述信息")
public final class PersonBean extends BaseRow
implements Serializable,Constant
{
private static final long serialVersionUID = 3364692410393104340L;
/** comments:用户id */
@ApiModelProperty(value = "用户id" ,required=true ,dataType="Integer")
@CodegenRequired@CodegenInvalidValue("0")
private Integer id;
/** comments:姓名 */
@ApiModelProperty(value = "姓名" ,required=true ,dataType="String")
@CodegenLength(max=32,prealloc=true)@CodegenRequired@CodegenInvalidValue
private String name;
/** comments:性别,0:女,1:男,其他:未定义 */
@ApiModelProperty(value = "性别,0:女,1:男,其他:未定义" ,dataType="Integer")
@CodegenInvalidValue("-1")
private Integer sex;
/** comments:用户密码,MD5 */
@ApiModelProperty(value = "用户密码,MD5" ,dataType="String")
@CodegenLength(value=32,prealloc=true)@CodegenInvalidValue
private String password;
/** comments:出生日期 */
@ApiModelProperty(value = "出生日期" ,dataType="Date")
@CodegenInvalidValue("0")
private java.util.Date birthdate;
/** comments:手机号码 */
@ApiModelProperty(value = "手机号码" ,dataType="String")
@CodegenLength(value=11,prealloc=true)@CodegenInvalidValue
private String mobilePhone;
/** comments:证件类型,0:未知,1:身份证,2:护照,3:台胞证,4:港澳通行证,5:军官证,6:外国人居留证,7:员工卡,8:其他 */
@ApiModelProperty(value = "证件类型,0:未知,1:身份证,2:护照,3:台胞证,4:港澳通行证,5:军官证,6:外国人居留证,7:员工卡,8:其他" ,dataType="Integer")
@CodegenInvalidValue("-1")
private Integer papersType;
/** comments:证件号码 */
@ApiModelProperty(value = "证件号码" ,dataType="String")
@CodegenLength(max=32,prealloc=true)@CodegenInvalidValue
private String papersNum;
/** comments:备注 */
@ApiModelProperty(value = "备注" ,dataType="String")
@CodegenLength(max=256)@CodegenInvalidValue
private String remark;
@ApiModelProperty(value = "create_time" ,dataType="Date")
@CodegenInvalidValue("0")
private java.util.Date createTime;
@ApiModelProperty(value = "update_time" ,dataType="Date")
@CodegenInvalidValue("0")
private java.util.Date updateTime;
////....../////
}
```
上面的代码中还出现了Swagger的`ApiModel`和`ApiModelProperty`注解(Annotation),可知这些注释内容也会同样被生成到Swagger在线文档中。
如下图红框标注的 `PersonBean`的字段说明

#### 字符集编码
`sql/create_table.sql`中定义了数据库的字符集编码为`utf-8`,如果非必要请不要修改
### 在数据库中创建表
在前面的示例中我们在`sql/create_table.sql`中增加一张`my_device`表,我们需要实际在数据库中创建`sql/create_table.sql`定义的这些表。
有两种方式,如果你安装了数据库对应的GUI工具(比如MySql的Workbench),或你更习惯用mysql client,你可以使用数据库提供的工具直接执行`sql/create_table.sql`脚本就可以完成建表动作。
如果你手头上暂时还没有这些工具,或者还不太熟悉使用,你可以在命令行执行`create_table.bat/create_table.sh`脚本来完成建表动作。
#### `create_table` 调用示例
Windows CMD 无参数执行示例
```shell
>create_table.bat
>mvn sql:execute
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building hello-world database(MySQL) operation library 0.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- sql-maven-plugin:1.5:execute (default-cli) @ hello-world-db ---
[INFO] Executing file: C:\Users\guyadong\AppData\Local\Temp\create_table.2770881
38sql
[INFO] 6 of 6 SQL statements executed successfully
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.337 s
[INFO] Finished at: 2021-12-06T14:00:11+08:00
[INFO] Final Memory: 19M/491M
[INFO] ------------------------------------------------------------------------
```
Linux 带命令行参数执行示例
```shell
$ ./create_table.sh -Ddb.username=hello -Ddb.password=world -Ddb.schema=test
```
#### `create_table` 命令行语法
```bash
create_table \
[-Ddb.driver=com.mysql.jdbc.Driver] \
[-Ddb.schema=test] \
[-Ddb.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&useSSL=false] \
[-Ddb.username=root] \
[-Ddb.password=] \
```
可选的命令行参数
> -Ddb.driver= ## 指定JDBC Driver类名,默认为com.mysql.jdbc.Driver
> -Ddb.schema= ## 指定数据库的schema,默认为test
> -Ddb.url= ## 指定数据库连接URL,默认为jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&useSSL=false
> -Ddb.username= ## 指定连接用户名,默认为root
> -Ddb.password= ## 指定连接用户密码,默认为空\
#### `sql-maven-plugin`插件
`create_table`脚本实际上是通过Maven的`sql-maven-plugin`插件来执行`sql/create_table.sql`脚本完成数据库建表的。
执行`sql-maven-plugin`插件需要的所有参数在[hello-world-db/pom.xml](template/myrpc-db/pom.xml)中都有定义默认参数。默认参数都以本地安装的mysql为基础,所以如果是在本地的mysql数据库建表,且root没设置密码,schema为test,以上参数都是可以忽略的。
`sql-maven-plugin`插件定义在[hello-world-db/pom.xml](template/myrpc/myrpc-db/pom.xml)
```xml
org.codehaus.mojo
sql-maven-plugin
1.5
mysql
mysql-connector-java
5.1.43
${db.driver}
${db.url}
${db.username}
${db.password}
default-cli
execute
${project.basedir}/sql/create_table.sql
```
### 生成数据库操作代码
上一步成功创建数据库表之后,就可以使用[`sql2java`](https://gitee.com/l0km/sql2java)插件来为这些表生成对应的数据库操作代码
----
#### gen-mysql.bat/sh
`gen-mysql` 脚本可以用来快速方便的生成基于mysql的数据库操作代码。**请参见下面的《JDBC连接配置》设置正确的数据库连接**,如果是在本地的mysql数据库建表,且root没设置密码,且schema为默认的test,直接执行`gen-mysql.bat/.sh`就可以生成对应的数据库操作代码了。
#### Windows CMD
```
gen-mysql.bat
```
#### Linux
```shell
$ ./gen-mysql.sh
```
`gen-mysql`执行结束,`my_person`及新增加的`my_device`表对应的数据库操作代码就如下保存在`hello-world/hello-world-db/src/main/java`下
```
└─hello-world
└─hello-world-db
│
└─src
└─main
├─java
│ └─com
│ └─mycompany
│ └─hello_world
│ └─db
│ Constant.java ## 数据库全局常量定义
│ DeviceBean.java ## my_deivce表对应的表记录类
│ DeviceMetaData.java ## my_deivce表对应的表元数据类
│ IDeviceManager.java ## my_deivce表对应的表操作类
│ IPersonManager.java ## my_person表对应的表操作类
│ PersonBean.java ## my_person表对应的表记录类
│ PersonMetaData.java ## my_person表对应的表元数据类
└─resources
├─conf
│ hello_world_database.properties
│
└─META-INF
└─services
gu.sql2java.IRowMetaData
```
### sql2java 参数配置
上一步生成数据库操作代码的工作实际上是由sql2java插件来完成的。
[sql2java](https://gitee.com/l0km/sql2java)是一个代码生成工具,用于根据指定的配置从数据库读取并分析表结构数据,以表为中心,将表的每条记录视为一个数据对象,为每张表生成对应的数据库操作代码,以减少开发者处理数据库操作的工作量和工作复杂度,提高代码质量。sql2java 工作时需要从`gen-mysql.properties`中读取相关的参数配置来生成代码,比如数据库连接配置,生成的包名,扩展模板等等。
#### JDBC连接配置
`gen-mysql.properties`中所有数据库连接配置参数名以`jdbc.`为前缀,在`gen-mysql.properties`中默认定义如下,你可以根据自己的实际情况进行修改:
```properties
#______________________________________________
#
# (1/8) CONFIGURE YOUR DATABASE ACCESS
#______________________________________________
jdbc.driver=com.mysql.jdbc.Driver
# If useInformationSchema=false or omited ,can not retrieve table COMMENT from MySQL
jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useInformationSchema=true
jdbc.username=root
jdbc.password=
jdbc.schema=test
```
> 关于`jdbc.url`中为什么要定义`useInformationSchema=true`
>
> 参见这篇博客 [《mysql/jdbc:设置useInformationSchema=true读取表注释信息(table_comment)》](https://blog.csdn.net/10km/article/details/77389038)
#### 表名前缀
同一个数据库实例中可能会存在多个项目的数据库表,我们只是希望sql2java为当前项目的相关的表生成数据库操作代码,所以为了让sql2java能区分并识别哪些是需要要生成数据库操作代码的目标表,我们需要在表结构定义时对每个表命名时加一个统一的前缀。
在前面的表结构定义示例`sql/create_table.sql`中,`my_person`,`my_device`表都有一个统一的前缀`my_`就是为了这个目的。
在sql2java 我们需要在`gen-mysql.properties`中通过`jdbc.tablenamepattern`参数告诉sql2java需要对哪些表生成代码。
如下,`jdbc.tablenamepattern=my_%` 即对所有以`my_`为表名前缀的表/视图生成数据库操作代码 ,`%`为SQL的通配符。
```properties
#______________________________________________
#
# (4/8) FILTER OUT CERTAIN TABLES
#______________________________________________
#
# Table name pattern of tables to be mapped to classes.
# available wildcard: %
# defaults to %
# You can specify several patterns separated by commas.
jdbc.tablenamepattern=my_%
```
#### BaseDao模板
为进一步减少开发者的代码量,在sql2java生成的数据库操作代码的基础上,`hello-world/hello-world-local/src/sql2java`下会生成一个`BaseDao`类,这个类以更简单的调用方式向开发者提供了数据库访问的基础方法,这个类是sql2java根据[`hello-world/hello-world-local/vm/custom/perschema/base.dao.java.vm`](template/myrpc-local/vm/custom/perschema/base.dao.java.vm)模板生成的。
`base.dao.java.vm`是基于[velocity](https://velocity.apache.org/)的模板语言VTL(Velocity Template Language)撰写的。
你暂时还不需要了解VTL是啥.但是你需要了解`base.dao.java.vm`文件最后面的一段代码的意义,表结构设计完成,一定要修改下面的内容,`BaseDao`类中才会生成对应表的操作代码,下面的代码中我们指定将新增加的`my_device`表以默认生成方式生成表操作代码,**修改了`base.dao.java.vm`后一定要执行gen-mysql才能生效**:
```velocity
## 默认生成方式:生成增删改查方法,适用于普通的数据库表,不能用于视图(VIEW)
#foreach($tname in ['my_person','my_device'])
#set($table = $db.getTable($tname))
#defineMethod($table false)
#end
## 只读生成方式:对于普通数据库表生成增删查方法(不允许修改),对于视图(VIEW)只生成查询方法
#foreach($tname in [])
#set($table = $db.getTable($tname))
#defineMethod($table true)
#end
```
对于`sql/create_table.sql`中定义的所有表和视图,如果希望同步在`BaseDao`中生成对应表操作方法,就要在`base.dao.java.vm`中修改修改上面的代码,将每个表名添加到`默认生成方式`或`只读生成方式`对应的数组中,如上例中,`my_persion`和增加的`my_device`表都作为普通表被添加到 `默认生成方式`的数组中。
- 普通表
一般来说,我们在设计表结构时大多会允许对表执行增删改查全部操作,这类的表我们可以叫它普通表
- 只读表
但有的时候,我们记录一张表的记录是不可修改的,也就是说允许添加,删除,和查询,但对已有数据不允许修改,这类表我们叫只读表,比如日志表
- 视图(VIEW)
视图(VIEW)并不是一个实体表,它没有存储,所以对于视图只能查询,所以说对于sql2java来说,视图也算只读表
#### 进阶:二进制数据类型(binary.type)
对于二进制数据在Java中通常用`byte[]`类型来保存,但为了保证数据库表记录对象在传递过程二进制数据字段能被正常序列化和反序列化,这里默认使用`java.nio.ByteBuffer`来存储二进制数据,非必要不要修改它。
```properties
# byte array mapping can be:
# byte[]
# java.nio.ByteBuffer
# default byte[] if not specialized
binary.type = java.nio.ByteBuffer
```
#### 进阶:日期类型
日期(`Date`)与`Long,Integer,String`这样的简单数据类型不同,是一种复杂数据类型,对于这种复杂类型在进行序列化和反序列化时的作法与简单类型是不同的,一般需要把复杂类型转化成一个可序列化的简单类型才能完成序列化,这个简单类型一般是整数类型或字符串类型。
日期在序列化成JSON字符串时,一般有两种实现方式:
- 将`Date`对象转成代表系统时间的整数(`Long`),然后再进行序列化
- 将`Date`对象转成特定日期格式(比如`yyyy-MM-dd HH:mm:ss`)的字符串(`String`),然后再进行序列化
sql2java在生成日期类型字段的代码时可以支持上面两种实现方式:
下面两个参数用于控制 `Date` 类型的JSON 序列化类型以及序列化为`String`时的日期格式
```properties
# Date 类型的JSON 序列化类型 :
# Long 系统时间(毫秒)
# String ISO8601 时间日期格式的字符串
# 当指定了 swift或jackon注解时有效
codewriter.bean.dateSerializeType = String
# 当 dateSerializeType 为 String 的日期格式,默认为IS8601 格式
#codewriter.bean.dateStringFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ
```
JavaScript是一种无类型的脚本语言,JavaScript解释器在处理所有数字类型变量都是做为`Double`对待的,所以如果将一个Long类型的整数赋值给JavaScript的变量,是有精度损失的,所以如果用上述的Long类型整数来代表时间传递JavaScript为前端的应用,会因为`Long`转`Double`的精度损失导致在前端显示的时间不正确。
为避免上面的问题,rpcgen默认生成的项目初始代码中,使用`String`为日期类型的JSON序列化类型。默认的日期字符串格式为ISO8601标准日期字符串,例如:`2021-12-11T21:52:00.153+0800`。
你可以根据需要修改`codewriter.bean.dateStringFormat`,提供自定义的日期格式。
以`my_person.birthdate`为例,在生成的`PersonBean`的代码关于`my_person.birthdate`字段序列化和反序列化相关的代码如下
`writeBirthdate`实现将一个日期字符串写入到`birthdate`字段(用于反序列化过程):
```java
/**
* setter for thrift:swift OR jackson support
* without modification for {@link #modified} and {@link #initialized}
* NOTE:DO NOT use the method in your code
*/
@ThriftField(name="birthdate")
@JsonProperty("birthdate")
public void writeBirthdate(String newVal){
/** parse date time string to Date object */
setBirthdate(gu.sql2java.Sql2javaSupport.parseDateString(newVal,java.util.Date.class));
}
```
`readBirthdate`则实现将`birthdate`字段转为`ISO8601`格式的日期字符串(用于序列化过程)
```java
/**
* use Long to represent date type for thrift:swift support
* @see #getBirthdate()
*/
@ThriftField(value=8,name="birthdate")
@JsonProperty("birthdate")
public String readBirthdate(){
/** output data time format string */
return gu.sql2java.Sql2javaSupport.formatDate(birthdate,"yyyy-MM-dd'T'HH:mm:ss.SSSZ");
}
```
#### 进阶:Annotation
sql2java的参数配置文件`gen-mysql.properties`中还有对生成代码的注解(Annotation)的配置
```properties
# facebook/swift annotation support for bean
# generate annotation for swift if true
swift.annotation.bean = true
# swagger annotation support for bean
swagger.annotation.bean = true
# 是否生成swagger注解
# jackson annotation support for bean
# 是否生成JACKSON注解
jackson.annotation.bean = true
# generate codegen annotation if true
# 是否生成codeg注解
codegen.annotation.bean = true
```
在你没有充分了解这些配置参数的含义之前,不要修改以上配置参数,保持默认值就可以了。
#### 进阶:自定义模板(velocity)
如果在执行`gen-mysql`时希望生成自定义的一些数据库相关代码,可以将`gen-mysql.properties`中的 `velocity.templates.extension`修改为如下。
```properties
# extensive tempate folder
velocity.templates.extension = vm;../hello-world-local/vm
```
- 这里的`vm`指的是`hello-world-db/vm`文件夹用于保存数据库操作相关的自定义模板代码
- 默认就有的的`../hello-world-local/vm`指的是`hello-world-local/vm`文件夹用于保存[BaseDao模板](####BaseDao模板),与服务接口实现且用到数据库操作的自定义模板也可以在这里实现
然后参照[facelog](https://gitee.com/l0km/facelog)中[db2/vm](https://gitee.com/l0km/facelog/tree/master/db2/vm)文件夹下的模板代码在`hello-world-db/vm`来撰写自定义模板。
### 进阶:自定义DAO 模板
在[《BaseDao模板》](####BaseDao模板])一节简单介绍了BaseDao模板生成代码的要求,[BaseDao](/template/myrpc-local/src/sql2java/java/myorg/myrpc/BaseDao.java)类为应用层提供了基本的数据库操作方法,因为它是放在`hello-world-local`的,所以这个模板可以引用`hello-world-local`的代码,开发者可以根据自己的需要在[`hello-world/hello-world-local/vm/custom/perschema/base.dao.java.vm`](template/myrpc-local/vm/custom/perschema/base.dao.java.vm)模板中增加自定义方法。
## 数据库操作
[sql2java](https://gitee.com/l0km/sql2java)是一个代码生成工具,它从数据库读取并分析表结构,以表为中心,将表的每条记录视为一个数据对象,为每张表生成对应的数据库操作代码,以减少开发者处理数据库操作的工作量和工作复杂度,提高代码质量。rpcgen生成的服务项目代码对数据库的操作都是基于sql2java生成的数据库操作代码来实现的。
本章开始介绍如果使用sql2java生成的数据库操作代码。
------
### 表记录对象(Java Bean)
sql2java生成数据库操作代码的设计思路是以表为中心,将表的每条记录视为一个数据对象,所以每张表都会生成一个数据记录类,对于前面示例中的my_person表生成的类名为`PersonBean`,这是个标准的[Java Bean](https://baike.baidu.com/item/javaBean),可以很方便的实现Java Object 序列化(serialize)和反序列化(deserialize)。也可以被JSON工具序列化为JSON字符串和从JSON字符串返序列化为对象。
如下图是sql2java生成`PersonBean`类代码在Eclipse中的大纲视图,每一个字段都生成了对应的驼峰命名的成员变量以及对应的getter/setter方法。

#### SQL 数据类型与Java 类型对照表
sql2java在生成表记录时,会根据字段的SQL类型生成对应的Java类型成员变量,下表是JDBC定义的数据类型(`java.sql.Types`)与sql2java最终生成的Java Class的对照表,中间是sql2java在运行时内部定义的映射类型常量。
| java.sql.Types | sql2java MappedType | Java Class Name |
| -------------- | --------------------------------- | -------------------------------------------------------- |
| VARCHAR | M_STRING | java.lang.String |
| VARBINARY | M_BYTES | byte[] or java.nio.ByteBuffer[^1] |
| TINYINT | M_INTEGER | java.lang.Integer |
| TIMESTAMP | M_UTILDATE/M_TIMESTAMP/M_CALENDAR | java.util.Date/java.sql.Timestamp/java.util.Calendar[^2] |
| TIME | M_UTILDATE/M_TIME/M_CALENDAR | java.util.Date/java.sql.Time/java.util.Calendar[^3] |
| STRUCT | M_OBJECT | java.lang.Object |
| SMALLINT | M_INTEGER | java.lang.Integer |
| REF | M_REF | java.sql.Ref |
| REAL | M_FLOAT | java.lang.Float |
| OTHER | M_OBJECT | java.lang.Object |
| NUMERIC | M_BIGDECIMAL/M_LONG | java.math.BigDecimal/java.lang.Long |
| LONGVARCHAR | M_STRING | java.lang.String |
| LONGVARBINARY | M_BYTES | byte[] or java.nio.ByteBuffer[^1] |
| JAVA_OBJECT | M_OBJECT | java.lang.Object |
| INTEGER | M_LONG/M_INTEGER | java.lang.Long/java.lang.Integer |
| FLOAT | M_DOUBLE | java.lang.Double |
| DOUBLE | M_DOUBLE | java.lang.Double |
| DISTINCT | M_OBJECT | java.lang.Object |
| DECIMAL | M_BIGDECIMAL/M_LONG | java.math.BigDecimal/java.lang.Long |
| DATE | M_UTILDATE/M_SQLDATE/M_CALENDAR | java.util.Date/java.sql.Date/java.util.Calendar[^4] |
| DATALINK | M_URL | java.net.URL |
| CLOB | M_CLOB | java.lang.String |
| CHAR | M_STRING | java.lang.String |
| BOOLEAN | M_BOOLEAN | java.lang.Boolean |
| BLOB | M_BLOB | byte[] or java.nio.ByteBuffer[^1] |
| BIT | M_BOOLEAN | java.lang.Boolean |
| BINARY | M_BYTES | byte[] or java.nio.ByteBuffer[^1] |
| BIGINT | M_LONG | java.lang.Long |
| ARRAY | M_ARRAY | java.sql.Array |
[^1]:由sql2java 参数`binary.type`控制,默认为`java.nio.ByteBuffer`
[^2]:由sql2java 参数 `jdbc2java.timestamp`控制,参见[日期类型](####日期类型)
[^3]:由sql2java 参数`jdbc2java.time`控制,参见[日期类型](####日期类型)
[^4]:由sql2java 参数`jdbc2java.date`控制,参见[日期类型](####日期类型)
#### 日期类型
SQL日期类型字段映射的Java类型由sql2java参数控制生成,如下表:
| java.sql.Types | sql2java property | | sql2java MappedType |
| -------------- | -------------------: | ---------------------- | ------------------- |
| TIME | jdbc2java.date= | java.util.Date[默认值] | M_UTILDATE |
| | | java.sql.Time | M_TIME |
| | | java.util.Calendar | M_CALENDAR |
| TIMESTAMP | jdbc2java.timestamp= | java.util.Date[默认值] | M_UTILDATE |
| | | java.sql.Timestamp | M_TIMESTAMP |
| | | java.util.Calendar | M_CALENDAR |
| DATE | jdbc2java.date= | java.util.Date[默认值] | M_UTILDATE |
| | | java.sql.Date | M_SQLDATE |
| | | java.util.Calendar | M_CALENDAR |
#### 辅助字段
在前面的截图中我们可以发现`PersonBean`中有3个并不是表结构中定义的字段`modified`,`initialized`,`isNew`
```java
/** columns modified flag */
@ApiModelProperty(value="columns modified flag",dataType="int",required=true)
private int modified;
/** columns initialized flag */
@ApiModelProperty(value="columns initialized flag",dataType="int",required=true)
private int initialized;
/** new record flag */
@ApiModelProperty(value="new record flag",dataType="boolean",required=true)
private boolean isNew;
```
sql2java生成的表记录类都会有这三个字段,这三个字段只存在于内存中,并不会保存到表记录,用于控制表操作向数据库中对象写入记录的动作
| 字段名 | 说明 |
| ----------- | ------------------------------------------------------------ |
| modified | 用于指定字段是否被修改过,每个字段占`modified`中的一bit,参见[《常量定义》](###常量定义)中关于字段掩码的说明,如果字段对应的bit为0则在保存记录到数据库时不会写入此字段.调用setter方法或`setValue`方法修改字段的值时会自动将对应的bit置1,从数据库读取的表记录对象或新创建的表记录对象所有的此字段全部为0. |
| initialized | 用于指定字段是否被初始化过(未初始化的字段必定为null),每个字段占initialized中的一bit,参见[《常量定义》](###常量定义)中关于字段掩码的说明.调用setter方法或`setValue`方法修改字段的值时会自动将对应的bit置1,从数据库读取的表记录对象会根据此字段是否有值自动置为0或1,新创建的表记录对象此字段如果有初始值则为1否则为0 |
| isNew | 为true时当前记录是一条新记录,否则是从数据库中读取的已存在记录,当表操作对象保存一个表记录对象到数据库时,如果此字段为true则执行INSERT操作,否则执行UPDATE操作,所以这个字段很重要一定不能填错。新创建的表记录对象此字段自动为true,从数据库读取的表记录对象此字段为false |
#### 创建新记录
sql2java生成的表记录类都有默认构造方法,所以如果创建新一个新记录对象最简单的方式就是`new` 操作符创建一个新对象。
```java
Person bean = new PersonBean();
bean.setName("tom");
bean.setMobilePhone("13000066666");
bean.setSex(1);
```
同时sql2java生成的表记录类也提供了builder模式构建新对象:
```java
Person bean = PersonBean.builder()
.name("tom")
.mobilePhone("13000066666")
.sex(1)
.build();
```
#### Java Bean读写
对sql2java生成的Java Bean的读写操作一般是通过getter/setter方法来完成。
如果字段的Java类型有对应的primitive类型,比如`Integer`,对应的primitive类型为`int`,为兼容性考虑sql2java会生成两个版本的setter方法,就如`PersonBean`中`sex`字段的`setter`方法就有`void setSex(Integer newVal)`和`void setSex(int newVal)`两个版本,这两个方法都可以使用:
```java
/**
* Setter method for {@link #sex}.
* The new value is set only if equals() says it is different,
* or if one of either the new value or the current value is null.
* In case the new value is different, it is set and the field is marked as 'modified'.
*
* @param newVal the new value to be assigned to sex
*/
@ThriftField(name="sex")
@JsonProperty("sex")
public void setSex(Integer newVal)
{
modified |= MY_PERSON_ID_SEX_MASK;
initialized |= MY_PERSON_ID_SEX_MASK;
if (Objects.equals(newVal, sex)) {
return;
}
sex = newVal;
}
/**
* Setter method for {@link #sex}.
* Convenient for those who do not want to deal with Objects for primary types.
*
* @param newVal the new value to be assigned to sex
*/
@JsonIgnore
public void setSex(int newVal)
{
setSex(new Integer(newVal));
}
```
- beModified
`beModified()`方法用于判断是否有字段被修改过,任何一个字段被修改过都会返回`true`。
同时sql2java会为每个字段生成一个`boolean`类型返回值的方法以方便开发者判断该字段有没有被修改过。
如`PersonBean.checkNameModified()`方法就用于判断`name`字段是否被修改过。
- resetIsModified
`resetIsModified`用于将清除所有字段的修改状态标记为0
- isModified
`isModified(int columnID)`方法允许使用字段的ID来判断指定字段是否有被修改过,字段ID定义参见Constant.java
- isInitialized
`isInitialized(int columnID)`允许使用字段的ID来判断指定字段是否有被修改过,字段ID定义参见Constant.java
- reset
`reset`将所有字段设置为初始值(一般为NULL),并将所有字段的修改状态设置为0
- clone
sql2java生成的表记录类支持`clone`方法
#### 表记录对象的父类BaseRow
为便于统一化操作,sql2java生成的表记录类(Java Bean)都有一个共同的父类`gu.sql2java.BaseRow`,该类其于字段名和和字段ID为所有表记录对象提供统一化的读写复制等操作。
| 方法名(可能有多个重载方法) | 说明 |
| -------------------------- | ------------------------------------------------------------ |
| getValue | 根据字段ID或字段名读取字段值 |
| setValue | 根据字段ID或字段名写入字段值 |
| primaryValues | 以数组形式返回主键值 |
| primaryValue | 如果主键只有一个字段,此方法返回主键值(否则抛出`UnsupportedOperationException`异常) |
| asValueArray | 以Object数组形式返回指定字段的所有值 |
| asNameValueMap | 以字段名-字段值映射形式(`Map`)返回字段所有值 |
| copy | 从另一个对象复制所有字段或指定字段的值到当前对象 |
| tableName | 返回当前对象对应的表名(例如 `my_person`) |
| compareTo | 为所有`BaseRow`的子类实现`java.lang.Comparable`接口,比较两个`BaseRow`是否相等 |
| equals/hashCode/toString | 为所有`BaseRow`的子类提供`equals/hashCode/toString`实现 |
#### 表的元数据(RowMetaData)
对每张表/视图,sql2java都会生成个保存表结构信息的元数据类,元数据对象保存的表的常量数据,用于描述表的结构及与其他表的关系。这个对象主要是提供给后续要讲到的表操作对象来完成对表的操作。
`PersonMetaData`就是`my_person`的元数据类,所有的表的元数据类都有一个共同的父类`gu.sql2java.RowMetaData`。
#### 序列化和反序列化
### 常量定义
[hello-world-db/src/main/java/your package/Constant.java](template/myrpc-db/src/main/java/myorg/myrpc/db/Constant.java)是sql2java生成的数据库常量定义类,定义了所有表的表名、字段名、字段ID等会被开发者用到的常量数据。
以`my_person`表为例说明`Constant.java`中定义的各类常量的意义
| 常量名 | 说明 |
| -------------------------- | ------------------------------------------------------------ |
| MY_PERSON_ID_$column | 字段ID,即表结构定义中每个字段在表中的顺序索引值(0-base) |
| MY_PERSON_ID_$column_MASK | 字段掩码,即(`1<<字段ID`计算的值),用于定义字段在initialized,modified中所占的bit |
| MY_PERSON_PK_FIELDS | 定义表的主键字段名列表,`,`分隔的字符串 |
| MY_PERSON_PK_FIELDS_LIST | 定义表的主键字段名列表,`List` |
| MY_PERSON_FULL_FIELDS | 带表名前缀的字段名列表,`,`分隔的字符串 |
| MY_PERSON_FULL_FIELDS_LIST | 带表名前缀的字段名列表,`List` |
| MY_PERSON_FIELDS | 字段名列表,`,`分隔的字符串 |
| MY_PERSON_FIELDS_LIST | 字段名列表,`List` |
| MY_PERSON_JAVA_FIELDS | 字段对应的Java成员名(camel-case)列表,`,`分隔的字符串 |
| MY_PERSON_JAVA_FIELDS_LIST | 字段对应的Java成员名(camel-case)列表,`List` |
| MY_PERSON_FIELD_TYPES | 字段对应的Java类数组 |
| MY_PERSON_FIELD_SIZES | 字段长度(byte)数组 |
| MY_PERSON_FIELD_SQL_TYPES | 字段类型对应的JDBC类型(`java.sql.types`)定义数组 |
> 所有数组List,`,`分割字符中数据的定义顺序与表结构定义一致
### 表操作对象(TableManager)
sql2java生成数据库操作代码的设计思路是以表为中心,所有对表的操作定义在表操作类(TableManager),通过表操作对象对来处理每个表记录对象,表操作类是接口类,从模板类`gu.sql2java.TableManager`继承。
#### TableManager
`gu.sql2java.TableManager`接口是个模板类,提供了对表的基础通用操作方法,见下表:
| 方法名 | 数据库操作 | 说明 |
| -------------------------------- | -------------- | ---------------------------------------------------- |
| createBean | | 创建表记录对象 |
| countAll | SELECT | 返回表记录数量 |
| countUsingTemplate | SELECT | 以指定表记录对象为模板统计符合条件的表记录数量 |
| countWhere | SELECT | 根据指定WHERE条件语句统计符合条件的表记录数量 |
| deleteAll | DELETE | 删除所有记录 |
| deleteByWhere | DELETE | 根据指定WHERE条件语句删除记录 |
| deleteUsingTemplate | DELETE | 以指定表记录对象为模板删除符合条件的表记录 |
| deleteByPrimaryKey | DELETE | 根据主键删除记录 |
| delete | DELETE | 删除指定表记录对象 |
| loadAll[AsList] | SELECT | 加载所有记录 |
| loadByPrimaryKey[Checked] | SELECT | 读取主键指定的记录 |
| existsPrimaryKey | SELECT | 判断是否存在主键指定的记录 |
| checkDuplicate | SELECT | 检查表中是否有重复记录 |
| loadByWhere[AsList] | SELECT | 加载WHERE条件语句指定的记录 |
| loadUniqueUsingTemplate[Checked] | SELECT | 以指定表记录对象为模板查找唯一记录(不唯一则抛出异常) |
| loadUsingTemplate[AsList] | SELECT | 以指定表记录对象为模板返回符合条件的表记录 |
| foreachByWhere | SELECT | 检索WHERE条件语句指定记录对每条记录执行指定的操作 |
| foreach | SELECT | 对每条记录执行指定的操作 |
| registerListener | | 注册表侦听器对象 |
| unregisterListener | | 删除指定的表侦听器对象 |
| save | INSERT\|UPDATE | 保存表记录对象到数据库 |
| addIfAbsent | INSERT | 如果指定的记录不存在则添加 |
| saveAsTransaction | INSERT\|UPDATE | 事务方式保存一组记录到数据库 |
| loadColumnAsList | SELECT | 检索WHERE条件语句指定记录的一个字段 |
#### IPersonManager
对于`my_person`表生成的表操作类名为`IPersonManager`,如下图,`IPersonManager`在`gu.sql2java.TableManager`的基础上提供了对`my_person`表更新加方便的调用方法,是对`gu.sql2java.TableManager`的补充和增强

| 方法名 | 数据库操作 | 说明 |
| ------------------------------- | ---------- | ------------------------------------------------------------ |
| loadByPrimaryKey[Checked] | SELECT | 读取主键指(`id`)指定的记录 |
| existsPrimaryKey | SELECT | 判断是否存在主键(`id`)指定的记录 |
| checkDuplicate | SELECT | 检查表中是否有重复记录 |
| deleteByPrimaryKey | DELETE | 读取主键(`id`)指定的记录 |
| loadByIndexMobilePhone[Checked] | SELECT | 通过`mobile_phne`(索引)字段查找记录 |
| deleteByIndexMobilePhone | DELETE | 删除`mobile_phne`(索引)字段指定的记录 |
| loadByIndexPapersNum | SELECT | 通过`papers_num`(索引)字段查找记录 |
| deleteByIndexPapersNum | DELETE | 删除`papers_num`(索引)字段指定的记录 |
| toPrimaryKeyList | | 将指定的表记录对象(集合/数组)只提取主键(`id`)字段转为对应的主键(`id`)集合/数组 |
#### DaoManagement
`BaseDao`是自动生成的代码,并不适合随时被开发者用户手工修改,`DaoManagement`是从`BaseDao`继承的可以由开发者自定义方法的类。开发者可以在此添加与自己业务需要相关的数据库操作方法,提供给其他模块使用。
#### 简单的数据库增删改查操作
本节以`my_person`表为例说明如何使用`DaoManagement`对象完或`IPersonManager`接口实例成基本的数据库增删改查操作
##### 增加记录
IPersonManager
```java
// my_person的主键id是自增长键,所以这里不能指定id由数据库生成,
PersonBean bean = PersonBean.builder().name("jerry").mobilePhone("13000008888").build();
// 执行保存记录操作
// 保存成功后,saved id字段保存的是数据自动生成的自增长id
PersonBean saved = Managers.instanceOf(IPersonManager.class).save(bean);
```
DaoManagement
```java
static final DaoManagement dm = new DaoManagement();
PersonBean bean = PersonBean.builder().name("jerry").mobilePhone("13000008888").build();
dm.daoSavePerson(bean);
```
##### 删除记录
IPersonManager
```java
// 执行保存记录操作
Managers.instanceOf(IPersonManager.class).deleteByIndexMobilePhone("13000008888");
```
DaoManagement
```java
static final DaoManagement dm = new DaoManagement();
dm.daoDeletePersonByIndexMobilePhone("13000008888");
```
##### 修改记录
IPersonManager
```java
// 获取表操作实例
IPersonManager personManager = Managers.instanceOf(IPersonManager.class);
PersonBean bean = personManager.loadByIndexMobilePhone("13000008888");
bean.setSex(1);
// 执行保存记录操作
personManager.save(bean);
```
DaoManagement
```java
static final DaoManagement dm = new DaoManagement();
PersonBean bean = dm.daoGetPersonByIndexMobilePhone("13000008888");
dm.daoSavePerson(bean);
```
##### 主键查询
IPersonManager
```java
PersonBean bean = Managers.instanceOf(IPersonManager.class).loadByPrimaryKey(1024/* 表记录主键(id 字段) */);
```
DaoManagement
```java
static final DaoManagement dm = new DaoManagement();
PersonBean bean = dm.daoGetPerson(1024/* 表记录主键(id 字段) */);
```
##### 索引查询
IPersonManager
```java
PersonBean bean = Managers.instanceOf(IPersonManager.class).loadByIndexMobilePhone("13000008888");
```
DaoManagement
```java
static final DaoManagement dm = new DaoManagement();
PersonBean bean = dm.daoGetPersonByIndexMobilePhone("13000008888");
```
##### 自定义查询(WHERE)
IPersonManager
```java
PersonBean bean = Managers.instanceOf(IPersonManager.class).loadByWhere("WHERE name='tom%'");
```
DaoManagement
```java
static final DaoManagement dm = new DaoManagement();
PersonBean bean = dm.daoLoadPersonByWhere("WHERE name='tom%'",1,0);
```
#### 执行自定义SQL
`gu.sql2java.TableManager`接口是个模板类,它还继承了`gu.sql2java.SqlRunner`接口。
`gu.sql2java.SqlRunner`接口为`gu.sql2java.TableManager`提供了执行自定义SQL语句的能力
| 方法名 | 说明 |
| ---------------- | ------------------------------------------------------------ |
| runSqlAsList | 执行返回数据库记录的SQL查询语句,返回的数据库记录保存在对象`BaseBean`列表 |
| runSqlForMap | 执行返回数据库记录的SQL查询语句,返回的数据库记录保存在字段名(String)-字段值(Object)映射中 |
| runSqlForValue | 执行返回值的SQL语句 |
| runAsTransaction | 以事务方式执行指定的语句 |
| runSql | 执行SQL语句,执行成功返回true,否则返回false |
调用示例:
```java
Integer count = Managers.instanceOf(IPersonManager.class).runSqlForValue("SELECT COUNT(id) from my_person");
```
### Cache同步问题[TODO]
### 异常处理
如果下图为sql2java定义的数据库相关异常的类型层次结构:

sql2java执行数据库操作时,所有的数据库相关异常都封装到`gu.sql2java.exception.RuntimeDaoException`抛出
### 表侦听器(TableListener)
[`gu.sql2java.TableListener`](https://gitee.com/l0km/sql2java/blob/master/sql2java-base/src/main/java/gu/sql2java/TableListener.java)接口用于应用层侦听数据库记录的变化,开发者实现此接口并将接口实例注册到对应的表记录对象即可以收到表记录操作的所有动作
> 参见 [TableManager](#TableManager) registerListener 和unregisterListener方法
| 方法名 | 说明 |
| ------------ | ---------------- |
| beforeInsert | INSERT操作前触发 |
| afterInsert | INSERT成功后触发 |
| beforeUpdate | UPDATE操作前触发 |
| afterUpdate | UPDATE成功后触发 |
| beforeDelete | DELETE操作前触发 |
| afterDelete | DELETE成功后触发 |
| done | 操作结束时触发 |
`gu.sql2java.TableListener`定义了6个接口方法分别侦听`INSERT,UPDATE,DELETE`操作,实际应用中开发者并不一定需要侦听所有操作,所以直接实现`gu.sql2java.TableListener`接口并不方便,建议通过继承`gu.sql2java.TableListener.Adapter`类来实现自己的侦听器。
## 服务接口设计
从本章节开始,我们着手在`hello-world`项目的基础定义自己的服务接口。
### 接口定义
**在实际开发中,可以完成服务接口实现再通过重构生成服务接口定义,所以建议阅读本章节前请先阅读下一章节《接口实现》**
#### IHelloWorld
`hello-world`项目的对外服务被定义成一个接口类`IHelloWorld`,保存在`hello-world-base` 子项目中.如下是`IHelloWorld`接口类的定义的方法列表:

除了上图中红框标注的三个方法外,其他方法都是了演示需要定义的方法,开发者在基于初始项目模板开发时可以根据自己的需要将这些方法删除或修改。
`version,versionInfo`两个方法用于显示当前服务的版本信息,代码由项目自动生成,建议保留。
`isLocal`方法必须保留。
##### 接口方法命名要求
作为OOP编程语言Java类中是允许方法重载(Overload)的,即允许同名但不同参数的多个方法定义。而在网络服务实现层在收到网络请求查找对应的服务方法端口(endPoint)时是按方法名来定位的,所以要求**服务接口的方法不能重名**。
所以在设计服务接口是**建议**不要使用方法重载。
##### @DeriveMethod
如果你确实喜欢用重载设计,那么就要在接口方法中使用`@DeriveMethod`注解来指定方法在生成服务接口名时的后缀,用不同的后缀来区分重载的方法。
下面的例子来自[facelog](https://gitee.com/l0km/facelog)的接口定义[IFaceLog.java](https://gitee.com/l0km/facelog/blob/master/facelog-base/src/main/java/net/gdface/facelog/IFaceLog.java),这里就用到不少方法重载。看看它是怎么避免服务方法端口重名的。
```java
/**
* 根据证件号码返回人员记录(脱敏数据)
* @param papersNum
* @return 没有找到匹配的记录则返回{@code null}
*/
public PersonBean getPersonByPapersNum(String papersNum);
/**
* 根据证件号码返回人员记录
* @param papersNum
* @param token 访问令牌
* @return 没有找到匹配的记录则返回{@code null}
*/
@DeriveMethod(methodSuffix="Safe")
public PersonBean getPersonByPapersNum(String papersNum, Token token);
```
`getPersonByPapersNum`有两个重载方法,为了在服务端口上区分,第二个方法使用`@DeriveMethod`注解指定了方法后缀`Safe`,这样以来第二个方法的生成的服务端口名为`getPersonByPapersNumSafe`,就避免了名字冲突。参见codegen生成的IFaceLog sprint controller类: [`net.gdface.facelog.IFaceLogSpringController`](https://gitee.com/l0km/facelog/blob/master/facelog-base/src/codegen/java/net/gdface/facelog/IFaceLogSpringController.java) 中对应`getPersonByPapersNum`(带`@DeriveMethod`注解)方法生成的服务端口(PORT)
```java
// port-111
@ResponseBody
@RequestMapping(value = "/IFaceLog/getPersonByPapersNumSafe", method = RequestMethod.POST)
@ApiOperation(value = "根据证件号码返回人员记录", notes = "根据证件号码返回人员记录",httpMethod="POST")
public Response getPersonByPapersNum( @RequestBody GetPersonByPapersNumSafeArgs args)
{
Response response = responseFactory.newIFaceLogResponse();
try{
response.onComplete(delegate().getPersonByPapersNum(args.papersNum,args.token));
}
catch(Exception e){
logger.error(e.getMessage(),e);
response.onError(e);
}
return response;
}
```
##### 接口方法注释
`IHelloWorld`服务运行时可以提供Swagger在线文档,你所看到的所有Swagger中的方法说明,参数说明都是由codegen代码生成工具从`IHelloWorld`类的源码注释是提取自动生成的。参见`IHelloWorldSpringController.java`
所以`IHelloWorld`接口的注释有多详细,Swagger就能提供多详细,开发者只要写好注释,剩下的交给codegen来完成。
Java Source

Swagger Document

#### 服务接口的实现类
如下是在Eclipse中导出的`IHelloWorld`接口及其实现类的类型层次结构图,图中显示了`IHelloWorld`所有的实现类

```mermaid
classDiagram
interface IHelloWorld
IHelloWorld --> HelloWorldDefaultImpl : 默认实现
IHelloWorld --> HelloWorldImpl : 本地实现
IHelloWorld --> IHelloWorldDecorator : 装饰类
IHelloWorld --> IHelloWorldThriftClient : Client 端实现(包括Android)
IHelloWorld --* IHelloWorldSpringController : SpringController代理
class IHelloWorld {
>>service>>
version()
isLocal()
versionInfo()
}
```
- HelloWorldDefaultImpl【所属项目:hello-world-base】
`IHelloWorld`接口默认实现,它没有做任何事,也不会在服务运行时被使用。它主要的作用是为[codegen](https://gitee.com/l0km/codegen)代码生成工具作为`IHelloWorld`接口的参考类(Reference Class),没有它`codegen`不能正常工作。
`HelloWorldDefaultImpl`就像人类的盲肠,作用不大,却不可缺少。
**在`IHelloWorld`添加或删除任何方法,都要确保`HelloWorldDefaultImpl`同步被修改。**
- HelloWorldImpl【所属项目:hello-world-local】
`IHelloWorld` 服务(本地)实现类,这是真正的实现服务接口的类,是所有服务接口方法的入口,后续会有更详细介绍。
- IHelloWorldDecorator[自动生成] 【所属项目:hello-world-base】
装饰者模式(decorator pattern )实现 `IHelloWorld`接口,简单说它就是个二传手,转发所有`IHelloWorld`接口方法到指定的`IHelloWorld`实例。
- IHelloWorldThriftClient[自动生成]【所属项目:hello-world-client】
基于thrift/swift框架生成的client端代码提供`IHelloWorld`接口的RPC实现,它在Java Client端基于thrift RPC机制实现了调用Server端`IHelloWorld`服务接口的 `IHelloWorld` 远程实例。为Java Client端提供了无语义差别的`IHelloWorld`调用服务。
- IHelloWorldThriftClient[codegen自动生成]【所属项目:hello-world-client-thrifty】
同上,适用于android平台。
- IHelloWorldSpringController[自动生成]【所属项目:hello-world-base】
这个类并没有实现`IHelloWorld`接口,它的作用是个代理:将一个`IHelloWorld`实例封装成Spring控制器(Controller),基于spring-web对外响应RESEful风格的HTTP服务请求。简单说它是所有HTTP请求的入口。所以在这里一并介绍。
> 通过上面的说明可以发现除了`HelloWorldImpl`和`HelloWorldDefaultImpl`类之外,其他的`IHelloWorld`实现或代理类都是由codegen自动生成的。所以对于开发者来说,只要关注`HelloWorldImpl`的实现就可以了。其他类的代码交给codegen帮你写。
### 接口实现
服务接口`IHelloWorld`的实现类`HelloWorldImpl`定义在`hello-world-local`子项目中。hello-world服务运行时不论使用什么协议访问服务,这`HelloWorldImpl`都是最终的服务入口。服务的所有业务逻辑都在这个类中实现。
本章以hello-world项目为例,以Eclipse为IDE,介绍如何对如何对生成的`IHelloWorld`接口重构,完成增加、删除、修改服务接口并实现服务接口方法逻辑的过程。
--------
#### 增加服务接口方法
下面我们在`HelloWorldImpl`中增加一个方法`quickAddPerson`实现根据用户名和移动电话号码及快速创建一个用户记录,以下是方法实现
```java
/**
* 根据用户名和移动电话号码及快速创建一个用户记录
* @param name 用户名,不允许为{@code null}
* @param mobilePhone 移动电话号码,不允许为{@code null}
* @return 创建的用户记录
*/
public PersonBean quickAddPerson(String name,String mobilePhone){
try{
PersonBean personBean = PersonBean.builder().
name(Preconditions.checkNotNull(name,"用户名字段不允许为NULL"))
.mobilePhone(Preconditions.checkNotNull(mobilePhone,"手机号码字段不允许为NULL"))
.build();
return dm.daoSavePerson(personBean);
} catch (Exception e) {
throw wrapServiceRuntimeException(e);
}
}
```
到目前为止`quickAddPerson`还不是`IHelloWorld`的接口方法,如下图在Eclipse中在增加的`quickAddPerson`方法名上点击鼠标右键,展开右键菜单找到【重构】子菜单点击【上拉】按钮,将`quickAddPerson`提升到`IHelloWorld`中。

会打开重构对话框,直接点击【完成】开始重构

忽略下面提示的问题,点击【完成】就可以完成代码重构,将`quickAddPerson`方法上拉到IHelloWorld服务接口定义中

如果在上面的界面中点击【下一步】则会显示重构操作要修改的源文件列表,再点击完成,同样完成代码重构。

重构完成后,如果还可以按`Ctrl-Z`反悔,会出现如下提示,点【确定】恢复重构所做的修改

查看一下 `HelloWorldDefaultImpl.java`发现它已经自动添加了`quickAddPerson`方法的实现,同样`IHelloWorld`服务接口的所有实现类都自动被修改了
至于将`IHelloWorld`封装为Spring Controller的`IHelloWorldSpringController`类还没有被同步修改,不用担心,这部分由codegen负责,后续章节计到代码生成会介绍。

然后查看一下`IHelloWorld`接口的代码,发现已经增加了方法定义`PersonBean quickAddPerson(String name, String mobilePhone);`,但是对应的方法注释却还留在`HelloWorldDefaultImpl.java`,所以增加的方法注释部分需要开发者手工移动到`IHelloWorld.java`中。
> Eclipse重构增加的接口方法定义没有`public`修饰符,这不影响代码的语义,因为java interface类中的方法不用`public`修饰符也是默认是`public`的。为明确语义考虑,开发者可以手工添加`public`修饰符。
#### 删除服务接口方法
删除接口方法就要手工完成了,开发者需要手工删除`IHelloWorld`,`HelloWorldDefaultImpl`中的方法定义,如果不想删除`HelloWorldImpl`中的方法实现,只需要删除方法实现的`@Override`注解就可以了。至于`IHelloWorld`其他的由codegen自动生成的实现类中的方法,开发者不需要手工删除,重新生成代码时会自动被删除。
#### 修改服务接口方法定义
以上面添加的`quickAddPerson`方法为例,在`HelloWorldImpl`或`HelloWorldDefaultImpl`中将光标放到`quickAddPerson`方法名或方法体内,按快捷键`Alt+Shift+C`或鼠标右键点击找到【重构/更改方法特征符(C)】点击就会弹出下面对话框,点击【确定】


如下,我们添加一个`sex`参数,然后点击【确定】就完成了代码重构

在上面的对话框中如果点击【预览】会显示重构将要修改的源码文件列表

重构结束后你只需在`IHelloWorld`中为`quickAddPerson`方法的新增加的参数`sex`补充注释就可以了。后续代码生成时会自动完成新增加的参数`sex`的调用处理。
当然还需要在`quickAddPerson`方法中将新增加的sex参数用上。
```java
@Override
public PersonBean quickAddPerson(String name,String mobilePhone, int sex){
try{
PersonBean personBean = PersonBean.builder().
name(Preconditions.checkNotNull(name,"用户名字段不允许为NULL"))
.mobilePhone(Preconditions.checkNotNull(mobilePhone,"手机号码字段不允许为NULL"))
.sex(sex)
.build();
return dm.daoSavePerson(personBean);
} catch (Exception e) {
throw wrapServiceRuntimeException(e);
}
}
```
#### 服务接口的异常处理
服务接口方法在调用时,难免会报错抛出异常,这种情况下,一般将异常信息在服务端以日志形式输出。
如果Client端能获取服务端详细的异常信息,更便于快速定位查找问题。
异常类`net.gdface.thrift.exception.ServiceRuntimeException`被设计用于封装服务端的异常,传递给Client端以实现上述的需求。`ServiceRuntimeException.printServiceStackTrace`方法用于输出服务端的调用堆栈信息。
参见上面的`quickAddPerson`方法的实现,它将所有的方法实现语句都放在`try-catch`异常捕获语句中。对捕获的异常调用`wrapServiceRuntimeException`方法将异常封装到`ServiceRuntimeException`抛出。这样当服务端抛出异常,Client端就可以获取详细的服务端异常信息,见下面Client调用的测试代码:
```java
public void test1SavePerson() {
IHelloWorlddClient helloWorldClient
= ClientFactory.builder()
.setHostAndPort("127.0.0.1", DEFAULT_PORT)
.build(IHelloWorldThriftClient.class, IHelloWorldClient.class);
PersonBean newPerson = PersonBean.builder().name("jerry").mobilePhone("13677448888").build();
try {
newPerson = helloWorldClient.savePerson(newPerson);
logger.info("person = {}", newPerson.toString());
PersonBean person = helloWorldClient.getPerson(newPerson.getId());
PersonBean p1 = helloWorldClient.getPersonByMobilePhone(person.getMobilePhone());
logger.info("person = {}", person.toString());
helloWorldClient.deletePerson(person.getId());
PersonBean p2 = helloWorldClient.getPersonByMobilePhone(person.getMobilePhone());
logger.info("{}",p2 != null);
} catch(ServiceRuntimeException e){
/** 捕获服务端,输出服务端异常堆栈 */
e.printServiceStackTrace();
assertTrue(false);
}catch (Exception e) {
logger.error(e.getMessage(), e);
assertTrue(false);
}
}
```
### 代码生成
一般情况下修改了服务接口定义就需要重新生成代码,只要执行`gen`就可以完成所有脚本的生成
> `gen`脚本能正常执行成功的前提是`hello-world-db,hello-world-base`这两个子项目能正常编译成功。
`hello-world-servcie`下所有`gen`为前缀的脚本都是用于项目代码生成的,每个脚本都可以独立执行。以下是每个脚本作用的说明:
> 数据库操作代码的生成除外,前面章节已经介绍过,数据库操作代码生成在`hello-world-db`下
| 脚本名(.bat/.sh) | 生成文件存储位置 | IHeloWorld实现类或Decorator | 说明 |
| ---------------------------- | ------------------------------------------- | --------------------------- | ------------------------------------------------------------ |
| gen-decorator-service | hello-world-service/src/codegen/java | IHelloWorldThriftDecorator | 生成IHelloWorldThriftDecorator,将IHelloWorld实例封装为一个thrift服务 |
| gen_thrift | hello-world-service/IHelloWorld.thrift | | 生成thrift 服务接口定义文件(IDL)(依赖gen-decorator-service执行结果) |
| gen_client | hello-world-client/src/thrift/java | | 根据IDL文件(IMyrpc.thrift)生成thrift stub代码(依赖gen_thrift执行结果) |
| gen_client-thrifty | hello-world-client-thrifty/src/thrifty/java | | 根据IDL文件(IMyrpc.thrift)生成thrifty stub代码(依赖gen_thrift执行结果) |
| gen-decorator-client | hello-world-client/src/codegen/java | IHelloWorldThriftClient | 生成IHelloWorld接口的基于facebook/swift的client实现代码(依赖gen-decorator-service执行结果), |
| gen-decorator-client-thrifty | hello-world-client-thrifty/src/codegen/java | IHelloWorldThriftClient | 生成IHelloWorld接口的基于Microsoft/thrifty的client实现代码(依赖gen-decorator-service执行结果) |
| gen | | | 执行上面所有脚本 |
下面是`gen.sh`的代码:
```bash
#!/bin/bash
sh_folder=$(cd "$(dirname $0)"; pwd -P)
pushd $sh_folder
./gen-decorator-service.sh && \
mvn install && \
./gen_thrift.sh && \
./gen_client.sh && \
./gen_client-thrifty.sh && \
./gen-decorator-client.sh && \
./gen-decorator-client-thrifty.sh
popd
```
可以看到`gen`是按照表格中的依赖顺序来顺序调用上面的子脚本的。并且执行完`gen-decorator-service`后要执行maven编译,编译成功才会执行后续的脚本。
在《服务接口的实现类》一节中介绍过服务接口有5个实现类,有3个是自动生成的,但上面的表格,只看到两个。`IHelloWorldDecorator`在哪里生成呢?还有`IHelloWorldSpringController`这个类是什么时候生成的呢?
`IHelloWorldDecorator`和`IHelloWorldSpringController`这两个类都定义在子项目`hello-world-base`中,不需要开发者手工生成,maven 编译`hello-world-base`时就会自动生成,参见[`hello-world-base/pom.xml`](template/hello-world-base/pom.xml)
```xml
com.gitee.l0km
codegen-decorator-maven-plugin
${codegen.version}
generate
com.mycompany.hello_world.IHelloWorld
com.mycompany.hello_world.HelloWorldDefaultImpl
com.mycompany.hello_world
${codegen.dir}
${project.basedir}/src/main/java
${maven.compile.classpath};${project.build.outputDirectory}
mbeaninfo.class.vm
${project.groupId}
${project.artifactId}
${project.version}
```
### 数据库访问基础方法(BaseDao)
`hello-world/hello-world-local/src/sql2java`下会的`BaseDao`类设计目的是为开发者提供更简单的数据库操作调用方式。本章节以`my_person`表为例介绍`BaseDao`生成的数据操作方法。
| 方法/变量名 | 说明 |
| ----------------------------------------- | ------------------------------------------------------------ |
| daoRunAsTransaction | 事务方式执行指定函数(Callable/Runnable) |
| daoLoadColumnAsList | 查询表的指定字段的数据 |
| personManager | IPersonManager my_person数据类实例 |
| daoGetPerson[Checked] | 根据person ID检索记录 |
| daoGetPersons | 根据一组person ID检索记录 |
| daoCastPersonToPk | 从一个PersonBean提取person ID字段(Integer)的Function实例 |
| daoCastPersonFromPk | 从一个person ID返回对应PersonBean的Function实例 |
| daoToPrimaryKeyListFromPersons | 将一组PersonBean对象中提取对应的person ID返回Integer列表 |
| daoExistsPerson | 判断person ID指定的记录是否存在 |
| daoDeletePerson | 删除主键指定的记录 |
| daoDeletePersonByIndexMobilePhone | 删除指定移动电话号码的记录 |
| daoDeletePersonByIndexPapersNum | 删除指定证件号码的记录 |
| daoDeletePersonsByPrimaryKey | 删除一组person ID指定的记录 |
| daoDeletePersons | 删除一组记录 |
| daoCheckDuplicate | 检查表中是否有重复记录 |
| daoGetDeviceBeansByOwnerIdOnPerson | 返回外键(my_device.owner_id)引用指定记录(my_person.id)的所有my_device记录 |
| daoDeleteDeviceBeansByOwnerIdOnPerson | 删除外键(idOfPerson))引用指定记录(my_person.id)的所有my_device记录 |
| daoSavePerson | 保存一条my_device记录[INSERT\|UPDATE] |
| daoSavePersons | 保存一组my_device记录[INSERT\|UPDATE] |
| daoSavePersonsAsTransaction | 以事务方式执行daoSavePersons |
| daoLoadPersonByWhere | 查询WHRE SQL条件语句指定的 my_person 记录 |
| daoLoadPersonUsingTemplate | 以指定表记录对象为模板返回符合条件的my_person记录 |
| daoLoadColumnOfPersonAsList | 查询 my_person 的指定字段的数据 |
| daoLoadPersonAll | 返回 my_person 表的所有记录 |
| daoCountPersonByWhere | 返回满足WHERE SQL条件语句的 my_person 记录总数 |
| daoLoadPersonIdByWhere | 查询WHERE SQL条件指定的my_person记录,返回person ID 列表 |
| daoLoadPersonByCreateTime | 返回 my_person.create_time 字段大于指定时间戳的所有记录 |
| daoGetPersonByIndexMobilePhone[Checked] | 根据移动电话号码查找 my_person 记录 |
| daoCheckDuplicatePersonByIndexMobilePhone | 根据移动电话号码检查数据库中是否有(唯一键)相同的记录,如果有则抛出异常 |
| daoGetPersonByIndexPapersNum[Checked] | 根据证件号码查找 my_person 记录 |
| daoCheckDuplicatePersonByIndexPapersNum | 根据证件号码检查数据库中是否有(唯一键)相同的记录,如果有则抛出异常 |
| daoLoadPersonByCreateTime | 根据记录创建时间戳(create_time)查找 my_person 记录 |
| daoCountPersonByCreateTime | 返回my_person.create_time 字段大于指定时间戳的记录总数 |
| daoLoadPersonByUpdateTime | 根据记录修改时间戳(update_time)查找 my_person 记录 |
| daoCountPersonByUpdateTime | 返回my_person.update_time 字段大于指定时间戳的记录总数 |
### 数据库操作扩展(DaoManagement)
`BaseDao`是自动生成的代码,并不适合随时被开发者用户手工修改,`DaoManagement`是从`BaseDao`继承的可以由开发者自定义方法的类。开发者可以在此添加与自己业务需要相关的数据库操作方法,提供给其他模块使用,也可以重写(override)`BaseDao`的方法。
### 数据库访问对象全局初始化(TableManagerInitializer)
`TableManagerInitializer`类负责数据库访问对象的全局初始化。
比如对表缓存的设置语句就是在`TableManagerInitializer`类的静态初始化区定义的,如果:
```java
// 如果配置文件中 database.cache.my_person.enable为true则对my_person表启用缓存
if(enableOf("my_person")){
Managers.registerCacheManager("my_person",updateStrategyOf("my_person"),maximumSizeOf("my_person"),durationOf("my_person"),TimeUnit.MINUTES);
}
```
开发者根据自己的项目需要,可以在`TableManagerInitializer`中增加自定义代码
### 配置参数管理
基于apache commons-configuration2,hello-world提供了配置文件管理机制。具体实现就是`GlobalConfig`类负责整个项目配置参数管理。
#### GlobalConfig
`GlobalConfig`以全局静态方法的形式提供配置文件管理的主要工具方法
| 方法名 | 说明 |
| ------------------ | ----------------------------------------------------- |
| getProperty | 返回指定的参数 |
| getProperties | 获取指定前缀的所有配置,以`Map`加 |
| setProperty | 修改指定的配置参数 |
| setProperties | 修改一组配置参数 |
| outputConfig | 输出配置参数到流 |
| persistence | 保存修改的配置到用户自定义配置文件 |
| getEnumList | 读取key指定的字符串并将其分割转换为指定的枚举对象列表 |
| getEnumSet | 读取key指定的字符串并将其分割转换为指定的枚举对象集 |
| descriptionOf | 返回key的描述信息 |
| makeDatabaseConfig | 从配置文件中读取数据库配置参数 |
#### 配置参数管理模型
[hello-world-local/src/main/resources/root.xml](template/myrpc-local/src/main/resources/root.xml)定义了配置管理基本框架。
root.xml
```xml
```
hello-world的配置参数由上面的xml文件定义的三个文件组成:
| 类型 | 位置 | 说明 |
| -------------- | ------------------------------------ | ------------------------------------------------------------ |
| User Config | $HOME/.hello-world/config.properties | HOME文件夹下的配置文件,如果不存在则自动从Default Config复制数据创建一个 |
| User Config | $HELLO_WORLD_HOME/config.properties | 环境变量HELLO_WORLD_HOME指定的文件夹下的配置文件,如果不存在则忽略 |
| Default Config | src/main/resources/defaultConfig.xml | 项目内置的配置文件,用于保存参数的默认值 |
上面三个文件的优先级从上而下由高到低。如果三个文件都定义了相同的参数,则以优先级最高的为准
##### Default Config
`defaultConfig.xml`用于定义配置参数的默认值,读取参数时,如果用户配置文件中没有定义指定的参数时,则从defaultConfig.xml对应的值代替。
开发者可以在`defaultConfig.xml`中根据需要定义自己的配置参数。
##### User Config
`$HOME/.hello-world/config.properties`是最常用的保存用户定义参数的方式。当服务启动时会检查此文件是否存在,如果不存在会自动从Default Config复制数据创建一个,以下是hello-world启动时默认创建的用户配置文件。所有的配置定义都用`#`禁用,用户需要设置某个值时,需要删除前导的`#`再设置定义的值就可以在下次服务启动时生效了。
```properties
# root帐户密码
#root.password=root
# 服务端口号
#server.port=28081
# 最大连接数
#server.connectiontLimit=32
# 空闲连接超时[秒]
#server.idleConnectionTimeout=60
# 工作线程数
#server.workerThreadCount=8
# XHR服务端是否启动
#xhr.start=true
# XHR服务端口
#xhr.port=28082
# RESTful 服务端是否启动
#restful.start=true
# RESTful服务端口
#restful.port=8080
# 是否显示在线swagger文档
#restful.swaggerEnable=true
# 是否支持跨域访问(CORS)
#restful.corsEnable=true
# 是否为调试状态
#database.isDebug=false
# 数据库服务器地址
#database.jdbc.host=localhost
# 数据库服务器端口
#database.jdbc.port=3306
# 数据库schema
#database.jdbc.schema=test
# 数据库字符编码方式
#database.jdbc.encoding=utf8
# 数据库连接URL
#database.jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false
# 数据库访问用户名
#database.jdbc.username=root
# 数据源类型
#database.datasource=c3p0
# 是否启用缓存
#database.cache.default.enable=false
# 更新策略,可选值:always,replace,remove,refresh
#database.cache.default.UpdateStrategy=always
# 最大缓存数量
#database.cache.default.maximumSize=10000
# 缓存记录保存时间(分钟)
#database.cache.default.duration=60
# fl_person表缓存配置
#database.cache.my_person.enable=true
# fl_person表缓存配置
#database.cache.my_person.UpdateStrategy=refresh
# 最大线程数量
#executor.cachedPool.maximumPoolSize=128
# 空闲线程保留时间(秒)
#executor.cachedPool.keepAliveTime=60
# 任务队列容量
#executor.cachedPool.queueCapacity=1024
# 线程命名格式
#executor.cachedPool.nameFormat=cached-pool-%d
# 线程数量,默认值:1
#executor.timerPool.corePoolSize=1
# 线程命名格式
#executor.timerPool.nameFormat=timer-pool-%d
# 是否输出sql2java详细异常信息
#syslog.op.coreDebug=false
```
#### 读取配置参数
```java
GlobalConfig.getConfig().getProperty("database.cache.my_person.enable");
```
如果你的类的引用了`ServiceConstant`,还可以少敲点代码,因为`ServiceConstant`有定义常量`CONFIG`.
```java
CONFIG.getProperty("database.cache.my_person.enable");
```
#### 修改配置参数
```java
GlobalConfig.getConfig().setProperty("database.cache.my_person.enable",true);
```
#### 持久化保存修改的参数
上一节修改的配置参数只是修改了内存中的数据,如果要保存到用户配置文件需要持久化。
```java
CONFIG.persistence();
```
#### 从配置文件中读取数据库配置参数
`GlobalConfig.makeDatabaseConfig()`方法会将数据库相关参数组成一个EnumMap返回:
```
EnumMap dbconfig = GlobalConfig.getConfig().makeDatabaseConfig();
```
### 服务版本信息(Version)
#### Version.java
提供版本信息的类[Version.java](template/myrpc-local/src/main/java/com/mycompany/hello_world/Version.java)从`version.properties`中读取版本相关数据,`version.properties`由maven在编译时自动生成
```java
package com.mycompany.hello_world;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import com.google.common.collect.ImmutableMap;
/**
* version information
*/
public final class Version {
/** project version */
public static final String VERSION;
/** SCM(git) revision */
public static final String SCM_REVISION;
/** SCM branch */
public static final String SCM_BRANCH;
/** build timestamp */
public static final String TIMESTAMP;
/** map of version fields */
public static final ImmutableMap INFO ;
static {
try(InputStream is = Version.class.getResourceAsStream("/version.properties")){
Properties properties=new Properties();
properties.load(is);
VERSION=properties.getProperty("VERSION");
SCM_REVISION=properties.getProperty("SCM_REVISION");
SCM_BRANCH=properties.getProperty("SCM_BRANCH");
TIMESTAMP=properties.getProperty("TIMESTAMP");
INFO = ImmutableMap.of(
"VERSION", VERSION,
"SCM_REVISION", SCM_REVISION,
"SCM_BRANCH",SCM_BRANCH,
"TIMESTAMP", TIMESTAMP);
} catch (IOException e) {
throw new ExceptionInInitializerError(e);
}
}
}
```
#### version,versionInfo
`IHelloWorld`接口中的`version,versionInfo`方法用于返回当前项目的版本信息。
```java
/**
* 返回服务版本号
* @return 服务版本号
*/
public String version();
/**
* 返回服务版本的详细信息
*
* - {@code VERSION} -- 服务版本号
* - {@code SCM_REVISION} -- GIT修订版本号
* - {@code SCM_BRANCH} -- GIT分支
* - {@code TIMESTAMP} -- 时间戳
*
*
* @return 服务版本的详细信息(参数名-参数值映射)
*/
public Map versionInfo();
```
在http://127.0.0.1:8080/swagger-ui.html中执行`version,versionInfo`会输出下面的结果
version

versionInfo

### 全局常量定义(CommonConstant)
`hello-world-base`下的`CommonConstant.java`用于定义全局常量,因为会被除`hello-world-db`之外的所有子项目依赖,所以在这里定义的全局常量可以被Server/Client共用。
### 服务端常量定义(ServiceConstant)
`hello-world-local`下的`ServiceConstant.java`用于定义服务端常量,只会被`hello-world-local`和`hello-world-service`使用。
### 服务端全局线程池
`hello-world-local`下的`ExecutorProvider`类用于配置服务端全局的线程池,`ExecutorProvider`提供的线程对象在应用程序结束时自动退出。所以对于开发者只管使用它,不需要负责维护线程池。
用户可以通过在用户配置文件中配置线程池相关参数来修改`ExecutorProvider`提供的线程池对象初始化参数,参见`CommonConstant`中`EXECUTOR_`前缀的相关常量定义:
```java
/** 通用线程池:最小线程数量,默认为CPU逻辑核数 */
public static final String EXECUTOR_CACHEDPOOL_COREPOOLSIZE = "executor.cachedPool.corePoolSize";
/** 通用线程池:最大线程数量 */
public static final String EXECUTOR_CACHEDPOOL_MAXIMUMPOOLSIZE = "executor.cachedPool.maximumPoolSize";
/** 通用线程池:空闲线程保留时间(秒) */
public static final String EXECUTOR_CACHEDPOOL_KEEPALIVETIME = "executor.cachedPool.keepAliveTime";
/** 通用线程池:任务队列容量 */
public static final String EXECUTOR_CACHEDPOOL_QUEUECAPACITY = "executor.cachedPool.queueCapacity";
/** 通用线程池:线程命名格式 */
public static final String EXECUTOR_CACHEDPOOL_NAMEFORMAT = "executor.cachedPool.nameFormat";
/** 定时任务线程池:线程数量,默认值:1 */
public static final String EXECUTOR_TIMERPOOL_COREPOOLSIZE = "executor.timerPool.corePoolSize";
```
## 服务运行
### 命令行启动
在开发阶段,用命令行启动服务进行测试是最常用的方式。
执行`hello-world-service/start_hello_world_server`脚本就可以在命令启动服务。
Windows CMD
```
start_hello_world_server.bat
```
Linux
```bash
./start_hello_world_server.sh
```
如果需要对服务进行远程调试,需要执行`start_hello_world_server_debug`以调试方式启动服务。
### 远程调试
执行`start_hello_world_server_debug`以命令行方式启动服务后就可以对服务进行远程调试。
Windows CMD
```
start_hello_world_server_debug.bat
```
Linux
```bash
./start_hello_world_server_debug.sh
```
远程调试方式启动后,JVM运行时会开启8000调试端口,如果你修改调试端口,需要修改`shell.template`下的`start_hello_world_server_debug.bat`和`start_hello_world_server_debug.sh`
[/hello-world-service/shell.template/start_hello_world_server_debug.bat](/template/myrpc-service/shell.template/start_myrpc_server_debug.bat)
```
SETLOCAL
set MYRPC_DEBUG_OPTS=-Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n
call %~dp0start_myrpc_server.bat %*
ENDLOCAL
```
[/hello-world-service/shell.template/start_hello_world_server_debug.sh](/template/myrpc-service/shell.template/start_myrpc_server_debug.sh)
```bash
#!/bin/bash
sh_folder=$(cd "$(dirname $0)"; pwd -P)
MYRPC_DEBUG_OPTS=-Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n \
$sh_folder/start_myrpc_server.sh $*
```
下面以Eclipse为例说明远程调试hello-world 服务的过程(服务已经启动)
首先在`HelloWorldImpl.quickAddPerson`方法中如下设置一个断点

鼠标点击打开菜单 [运行/调试配置]

打开 http://127.0.0.1:8080/swagger-ui.html#!/IHelloWorld32Controller/quickAddPersonUsingPOST 如下图输入服务方法参数,点击`Try it out!` 发送请求

Eclipse中触发断点收到HTTP Client端的请求

请求结束的返回数据如下图:

### 创建服务的Fat-Jar
Fat-Jar是指包含了Java程序编译生成的所有目标文件(.class等)及其所有依赖库的Jar包,也叫uberJar,一般Fat-Jar的体积都比较大。它的好处是当Java库或Java应用程序被打成Fat-Jar交付给用户使用时,运行Java应用程序或引用Java库就不再需要其他的依赖库。如果一个Java服务打包成Fat-Jar,那么只需要JRE支持就可以启动服务。对运行环境的依赖就非常低。
hello-world提供了为应用服务创建Fat-Jar的脚本`shade-package`(`hello-world-service`子项目下),在《Maven编译打包》一节已经介绍了命令行执行`shade-package`脚本完成Maven编译打包的过程。
`shade-package`生成的Fat-Jar包`hello-world-service-0.0.0-SNAPSHOT-standalone.jar`只需要有JRE支持就可以运行
```shell
java -jar hello-world-service-0.0.0-SNAPSHOT-standalone.jar
```
### 创建发行包
`shade-package`脚本生成的`hello-world-service-0.0.0-SNAPSHOT-standalone.jar`中已经包含了运行`hello-world`服务所需要的所有必要程序。
只需要执行`java -jar hello-world-service-0.0.0-SNAPSHOT-standalone.jar`就可以启动服务。但是让用户自己敲这一长串代码启动服务,还真是很为难了。
一个完整的产品,不能这么草率的交付。应该为使用方考虑,尽可能提供更加便利的安装部署方式,以一个完整,明确的数据包提供所有的产品数据。
所以为了更加简化服务的启动,以及简化服务安装和部署,hello-world 还提供了一些便利化的脚本,以及服务说明。这些文件与Far-Jar一起打包,交付用户,才算是一个完整的发包(distributable package)。
所以打包是个技术活儿,也是个体力活儿。为了让开发者从这些体力活儿中解脱出来。hello-world提供了命令行脚本程序`dist.sh`专门用于创建服务的发行包,即将服务的Fat-Jar,命令行启动脚本,服务部署脚本,服务说明等文件打在一个压缩包中。
Bash Shell(Linux,MacOs,Windows/MSYS(git bash)下执行`hello-world-service/dist.sh`即完成发行包创建。
```bash
$ ./dist.sh
hello_world_version=0.0.0-SNAPSHOT
crate distributable package (创建发行包) 0.0.0-SNAPSHOT...
copy file from /j/rpcgen/target/hello-world/hello-world-service/hello-world-service to hello-world-service
....
生成压缩包 hello-world-service-0.0.0-SNAPSHOT.tar ...
...
生成压缩包 hello-world-service-0.0.0-SNAPSHOT.tar.bz2 ...
distributable package is available(发行包已生成):/j/rpcgen/target/hello-world/hello-world-service/build/hello-world-service-0.0.0-SNAPSHOT.tar.bz2
```
如下是生成的`hello-world-service-0.0.0-SNAPSHOT.tar.bz2`的内容,所有文件都来自`hello-world-service`子项目,并保持相同的位置
```
└─hello-world-service-0.0.0-SNAPSHOT
└─hello-world-service
│ hello_world ## bash服务启动脚本
│ hello_world.service.in ## Linux systemctl 服务配置模板
│ install.sh ## Linux systemctl 服务安装脚本
│ README.md ## 服务说明
│ start_hello_world_server.bat ## 命令启动服务脚本(Windows CMD)
│ start_hello_world_server.sh ## 命令启动服务脚本(bash shell)
│ start_hello_world_server_debug.bat ## 命令(调试)启动服务脚本(Windows CMD)
│ start_hello_world_server_debug.sh ## 命令(调试)启动服务脚本(bash shell)
│
└─target
hello-world-service-0.0.0-SNAPSHOT-standalone.jar ## Fat-Jar
start_hello_world_server.bat ## 命令启动服务脚本(Windows CMD)
start_hello_world_server_debug.bat ## 命令(调试)启动服务脚本(Windows CMD)
start_hello_world_server_debug.sh ## 命令(调试)启动服务脚本(bash shell)
start_hello_world_server.sh ## 命令启动服务脚本(bash shell)
```
> 在Windows/MSYS下执行dist.sh脚本时,会自动将添加到发行包中的所有shell脚本中的Windows换行符('\r\n')为Unix换行符('\n').所以在Windows创建的发行包可以在Linux下直接使用。``
### 服务部署(Linux)
hello-world提供了基于Linux systemctl的服务部署脚本。
执行`hello-world-service/install.sh`即可以完成服务部署,如下在Linux系统中创建了一个名为`hello_world`的服务:
```bash
$ ./install.sh
创建 hello_world 服务脚本:/etc/systemd/system/hello_world.service
WorkingDirectory=/home/gyd/workspace/rpcgen/target/hello-world/hello-world-service
ExecStart=/home/gyd/workspace/rpcgen/target/hello-world/hello-world-service/hello_world start --hup
ExecStop=/home/gyd/workspace/rpcgen/target/hello-world/hello-world-service/hello_world stop
User=gyd # 执行服务的用户名【当前用户】
Group=gyd # 执行服务的用户组【当前用户的组】
```
> 如上显示,`hello_world.service`配置为以执行`install.sh`安装服务的用户环境执行,以`install.sh`所在位置为工作目录,启动服务即执行`hello_world start --hup`。停止服务执行`hello_world stop`
执行`systemctl status hello_world`查看安装的服务的运行状态状态显示服务已经启动(`active running`):

因为需要向Linux系统安装服务,所以要求当前用户为sudoer,有sudo权限(不建议用root用户执行部署)。
服务部署完成后会在系统目录生成`/etc/systemd/system/hello_world.service`。
### 服务管理(systemctl)
上一节完成了服务部署,后续就可以用`systemctl`来控制服务的启动与停止
查看服务状态
```bash
systemctl status hello_world
```
重启服务
```bash
sudo systemctl restart hello_world
```
停止服务
```bash
sudo systemctl stop hello_world
```
删除服务
```bash
sudo systemctl disable hello_world
```
查看服务日志
```bash
tail -f hello-world-service/hello_world.out
```
> 如果需要修改日志文件输出位置,请在`hello-world-service/hello_world`脚本中修改
### JVM参数
如果开发者需要在启动服务的时候,通过JVM参数控制服务的运行参数,不必修改直接修改服务启动脚本。
可以通过环境变量`HELLO_WORLD_OPTS`来传递JVM参数。
如下是服务命令行启动脚本(bash)的实现,可以看到执行该脚本的时候,会自动将`HELLO_WORLD_OPTS`环境变量的值作为JVM参数传递给运行的JVM虚拟机。Window版本的脚本也是一样的逻辑。
```bash
#!/bin/bash
sh_folder=$(cd "$(dirname $0)"; pwd -P)
pushd $sh_folder > /dev/null 2>&1
java $HELLO_WORLD_OPTS $HELLO_WORLD_DEBUG_OPTS -jar ${project.build.finalName}-standalone.jar $*
popd > /dev/null 2>&1
```
### 自定义text logo
服务命令行启动时会输出服务的text logo
```
ooo. .oo. .oo. oooo ooo oooo d8b oo.ooooo. .ooooo.
`888P"Y88bP"Y88b `88. .8' `888""8P 888' `88b d88' `"Y8
888 888 888 `88..8' 888 888 888 888
888 888 888 `888' 888 888 888 888 .o8
o888o o888o o888o .8' d888b 888bod8P' `Y8bod8P'
.o..P' 888
`Y8P' o888o
```
它保存在 `hello-world-service/src/main/resources/logotext.txt`
这是由文本图像工具生成的,你可以去 http://patorjk.com/software/taag/ 自己生成一个,覆盖上面的同名文件就可以了。
搜索引擎中搜索 `Text Art` 关键字,你能找到更多其他的网站满足你的需要。
### 在线文档(Swagger)
Swagger 是一个规范且完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
Swagger 的目标是对 REST API 定义一个标准且和语言无关的接口,可以让人和计算机拥有无须访问源码、文档或网络流量监测就可以发现和理解服务的能力。当通过 Swagger 进行正确定义,用户可以理解远程服务并使用最少实现逻辑与远程服务进行交互。与为底层编程所实现的接口类似,Swagger 消除了调用服务时可能会有的猜测。
Swagger 的优势
- 支持 API 自动生成同步的在线文档:使用 Swagger 后可以直接通过代码生成文档,不再需要自己手动编写接口文档了,对程序员来说非常方便,可以节约写文档的时间去学习新技术。
- 提供 Web 页面在线测试 API:光有文档还不够,Swagger 生成的文档还支持在线测试。参数和格式都定好了,直接在界面上输入参数对应的值即可在线测试接口。
Swagger 好是好,不过Swagger 是通过注解的方式来生成对应的 API,在接口上我们需要加上各种注解来描述这个接口,Swagger才能正确生成在线文档,所以虽然Swagger可以自动生成文档,但文档的内容还需要开发者以Swagger注解(annotation)的方式写在服务接口方法上才行。
这对于开发者来说是不个小的工作量,再说了,我们在写代码的时候都会有写注释啊,这些注释就是对方法描述。为什么不能源码注释中提取内容生成Swagger文档呢?
额,代码生成工具[codegen](https://gitee.com/l0km/codegen)在从服务接口生成SpringController时会从服务接口源码中提取注释并将之以Swagger的注释的方式生成的SpringController的源码中。
`prjgen.sh`就是基于codegen生成的项目源码框架的,在这个源码框架下开发,开发者只要写好服务接口方法的注释,Swagger文档生成的工作就交给codegen完成好了。
如下图,`IHelloWorldSpringController.java`是基于服务接口`IHelloWorld.java`接口类生成的

在下面对比截图就可以看到codegen在生成`IHelloWorldSpringController.java`中的`deletePerson`服务方法时`@ApiOperation`注释中的内容就是从`IHelloWorld.java`中`deletePerson`方法的代码中原样复制的。

默认情况下,Swagger在线文档是自动开启的,服务正常启动后,访问 http://127.0.0.1:8080/swagger-ui.html 就可以看到hello-world的swagger在线文档了

Swagger在线文档一般是用于开发阶段为前端开发人员提供技术支持,在生产环境中并不一定需要对外提供Swagger在线文档,通过设置服务启动参数,可以关闭Swagger在线文档。参见后面的服务参数配置章节的说明。
## 服务参数配置
### 服务端口配置
初始状态,服务启动后会自动生成用户服务配置参数文件:`$HOME/.hello_world/config.properties`,文件中关于服务端口配置的参数如下:
可以根据需要编辑`config.properties`修改些参数,保存后在下次服务启动时生效:
```properties
# 服务端口号
#server.port=28081
# 最大连接数
#server.connectiontLimit=32
# 空闲连接超时[秒]
#server.idleConnectionTimeout=60
# 工作线程数
#server.workerThreadCount=8
# XHR服务端是否启动
#xhr.start=true
# XHR服务端口
#xhr.port=28082
# RESTful 服务端是否启动
#restful.start=true
# RESTful服务端口
#restful.port=8080
# 是否显示在线swagger文档
#restful.swaggerEnable=true
# 是否支持跨域访问(CORS)
#restful.corsEnable=true
```
### 数据库参数配置
#### config.properties
hello-world服务允许通过用户配置参数文件`config.properties`设置数据库连接参数
hello-world在服务启动运行时会自动生成一个服务的用户参数配置文件,位置在`$HOME/.hello-world/config.properties`
其中数据连接相关的参数如下:
```properties
# 是否为调试状态
#database.isDebug=false
# 数据库服务器地址
#database.jdbc.host=localhost
# 数据库服务器端口
#database.jdbc.port=3306
# 数据库schema
#database.jdbc.schema=test
# 数据库字符编码方式
#database.jdbc.encoding=utf8
# 数据库连接URL
#database.jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false
# 数据库访问用户名
#database.jdbc.username=root
```
上面的参数定义`=`号后面就是它们默认的值,如果用户不指定,服务启动时就会使用默认值。
每一个参数设置都是用`#`注释的,如果要设置某个参数需要删除前导`#`,再设置指定的值。
> 数据库连接参数的默认值在[hello-world-local/src/main/resources/defaultConfig.xml](/template/myrpc-local/src/main/resources/defaultConfig.xml)中定义
#### database.properties
使用[`sql2java`](https://gitee.com/l0km/sql2java)生成数据库操作代码时你会在如下位置找到`${projectname}_database.properties`的文件,这个文件是sql2java在生成代码时生成的数据库连接配置文件。
hello-world/hello-world-db/src/main/resources/conf/hello_world_database.properties
```properties
// ______________________________________________________
// Generated by sql2java - https://github.com/10km/sql2java
// JDBC driver used at code generation time: com.mysql.jdbc.Driver
// template: database.properties.vm
// ______________________________________________________
#configuation for database connection
#whether use debug config.set to 'true' use config with 'debug' prefix,otherwise use 'work' config.
isDebug=false
alias=hello_world
#work parameter
work.jdbc.driver=com.mysql.jdbc.Driver
work.jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useInformationSchema=true
work.jdbc.username=root
work.jdbc.password=
work.c3p0.minPoolSize=10
work.c3p0.maxPoolSize=100
work.c3p0.maxIdleTime=120
work.c3p0.idleConnectionTestPeriod=120
#debug parameter
debug.jdbc.driver=com.mysql.jdbc.Driver
debug.jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useInformationSchema=true
debug.jdbc.username=root
debug.jdbc.password=
debug.c3p0.minPoolSize=10
debug.c3p0.maxPoolSize=100
debug.c3p0.maxIdleTime=120
debug.c3p0.idleConnectionTestPeriod=120
```
上面的连接参数`jdbc.driver,jdbc.url,jdbc.username,jdbc.passworld`的值都是在执行gen-mysql时由用户提供的连接参数。sql2java在初始化数据库连接时,如果外部没有注入连接参数,会默认从这里读取连接参数。
> 参见 [`gu.sql2java.DataSourceConfig`](https://gitee.com/l0km/sql2java/blob/master/sql2java-manager/src/main/java/gu/sql2java/DataSourceConfig.java)的默认构造方法及`getDatabaseproperties()`方法实现
这些连接参数只是开发阶段用到的数据库连接,实际部署运行环境的连接一般与开发环境是不同的。所以这种以代码方式写进Java Jar包的连接参数显示只能用于开发环境不能用于运行环境。
### 表缓存设置
[`sql2java`](https://gitee.com/l0km/sql2java)实现了JVM缓存机制,即以主键和唯一键为key提供数据库记录的快速查询,如果指定的主键或唯一键记录已经在缓存中,则直接返回,而不需要从数据库中查询。如果通过sql2java调用修改了数据库记录,缓存中的记录也会同步修改。
hello-world服务允许通过用户配置参数文件`config.properties`控制是否开启表的缓存以及最大缓存数量,缓存记录保存时间等参数
hello-world在服务启动运行时会自动生成一个服务的用户参数配置文件,位置在`$HOME/.hello-world/config.properties`
其中表缓存机制相关的参数如下:
```properties
# 是否启用缓存
#database.cache.default.enable=false
# 更新策略,可选值:always,replace,remove,refresh
#database.cache.default.UpdateStrategy=always
# 最大缓存数量
#database.cache.default.maximumSize=10000
# 缓存记录保存时间(分钟)
#database.cache.default.duration=60
#database.cache.my_person.enable=true
#database.cache.my_person.UpdateStrategy=refresh
```
上面的参数定义`=`号后面就是它们默认的值,如果用户不指定,服务启动时就会使用默认值。
每一个参数设置都是用`#`注释的,如果要设置某个参数需要删除前导`#`,再设置指定的值。
> 数据库缓存设置参数的默认值在[hello-world-local/src/main/resources/defaultConfig.xml](/template/myrpc-local/src/main/resources/defaultConfig.xml)中定义,
>
> defautlConfig.xml中可以对每张表的设置默认的缓存控制参数.参见对my_person的设置示例
### 日志配置
hello-world使用log4j为日志输出工具。开发者可以直接修改`log4j.properties`,也可以在用户参数配置文件:`$HOME/.hello_world/config.properties`,修改下面的参数来控制日志输出。
```properties
# 是否输出sql2java详细异常信息
#syslog.op.coreDebug=false
# log4j相关配置参数,不指定则使用log4j.properties中的值
# 对应 log4j key :log4j.rootCategory 中的level,系统日志级别 [OFF,FATAL,ERROR,WARN,INFO,DEBUG,ALL]
#syslog.level=
# 对应的 log4j key :log4j.appender.LOGFILE.File,系统日志文件位置
#syslog.location=
```
`helllo-world-local/src/main/resources/log4j.properties`为默认的log4j配置文件
## RPC Client
### Java
本节介绍Java Client的使用,hello-world服务提供了Java Client(hello-world-client)及Java Thrifty Client(hello-world-client-thrifty)两个版本的Client代码分都可以用于通用Java Client平台,Java Thrifty Client还可以用于Android Java平台。因为这两个版本的用户接口完全一致,所以合并在一起介绍以及统称Java Client
如果Java Client调用hello-world服务,可以直接使用hello-world的Java Client 库来访问,相比使用HTTP服务接口更加方便:
Java Client同样实现了`IHelloWorld`接口,实现类为`IMyrpcClient`, 以下为 Java Client 调用`IMyrpcClient`的示例代码:
```java
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class ClientTest implements CommonConstant {
public static final Logger logger = LoggerFactory.getLogger(ClientTest.class);
private static IHelloWorldClient hello_worldClient;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
// 创建 Client 实例
hello_worldClient = ClientFactory.builder()
.setHostAndPort("127.0.0.1", DEFAULT_PORT)
.build(IHelloWorldThriftClient.class, IHelloWorldClient.class);
}
@Test
public void test1SavePerson() {
PersonBean newPerson = PersonBean.builder().name("jerry").mobilePhone("13677448888").build();
try {
newPerson = hello_worldClient.savePerson(newPerson);
logger.info("person = {}", newPerson.toString());
PersonBean person = hello_worldClient.getPerson(newPerson.getId());
PersonBean p1 = hello_worldClient.getPersonByMobilePhone(person.getMobilePhone());
logger.info("person = {}", person.toString());
hello_worldClient.deletePerson(person.getId());
PersonBean p2 = hello_worldClient.getPersonByMobilePhone(person.getMobilePhone());
logger.info("{}",p2 != null);
} catch(ServiceRuntimeException e){
e.printServiceStackTrace();
assertTrue(false);
}catch (Exception e) {
logger.error(e.getMessage(), e);
assertTrue(false);
}
}
}
```
上面示例的完整代码生成在`/hello-world-client/src/test/java/com/mycompany/hello_world/client/ClientTest.java`
### C++
rpcgen在生成Java Client代码的时候也可以同时生成C++ client代码。生成的C++11标准代码支持跨平台编译,编译方式参见 [hello-world-client-cpp/README.md](template/myrpc-client-cpp/README.md)
### CSharp
如果项目需要为用户提供CSharp Client,使用 Swagger提供的Client端代码生成工具`swagger-codegen`生成CSharp client代码,生成的CSharp client代码通过hello-world服务的HTTP服务接口(RESTful Web API)与hello-world服务交互。
`swagger-codegen`生成的是一个完整的包含Visual Studio编译工程文件的CSharp代码,将生成的CSharp 代码用Visual Studio编译成动态库,就可以为CSharp 项目调用。下介绍`swagger-codegen`的生成CSharp Client的过程。
运行`swagger-codegen` 需要JDK 7(及以上)支持.
#### 下载 swagger-codegen
从maven中央仓库下载 `swagger-codegen` (io.swagger:swagger-codegen-cli:2.4.20)
下载位置:https://repo1.maven.org/maven2/io/swagger/swagger-codegen-cli/2.4.20/swagger-codegen-cli-2.4.20.jar
Linux或MacOS下可以用`wget`下载:
```bash
wget https://repo1.maven.org/maven2/io/swagger/swagger-codegen-cli/2.4.20/swagger-codegen-cli-2.4.20.jar
```
#### 生成代码
`swagger-codegen-cli-2.4.20.jar`是一个可以独立运行的Java程序,只需要有JRE支持就可以支行,如下命令行执行`swagger-codegen-cli`生成CSharp代码:
Windows CMD:
java -jar swagger-codegen-cli-2.4.20.jar generate ^
-i http://http://127.0.0.1:8080/v2/api-docs ^
-l csharp ^
--api-package hello_world --model-package hello_world.model ^
-o hello-world-client-csharp
Linux:
```bash
java -jar swagger-codegen-cli-2.4.20.jar generate \
-i http://127.0.0.1:8080/v2/api-docs \
-l csharp \
--api-package hello_world --model-package hello_world.model \
-o hello-world-client-csharp
```
> `127.0.0.1:8080`为运行的hello-world服务主机名和HTTP端口号。`swagger-codegen`将从运行的hello-world服务的Swagger在线文档中获取服务接口信息,据此生成CSharp Cient代码。
>
> `hello-world-client-csharp` 为指定生成代码的输出文件夹
`swagger-codegen`运行输出:
```
[main] INFO io.swagger.parser.Swagger20Parser - reading from http://127.0.0.1:8080/v2/api-docs
[main] INFO io.swagger.codegen.languages.CSharpClientCodegen - Generating code for .NET Framework v4.5
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\CountPersonByWhereArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\CountPersonByWhereArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\CountPersonByWhereArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\DeletePersonArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\DeletePersonArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\DeletePersonArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\ExistsPersonArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\ExistsPersonArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\ExistsPersonArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\GetPersonArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\GetPersonArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\GetPersonArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\GetPersonByMobilePhoneArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\GetPersonByMobilePhoneArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\GetPersonByMobilePhoneArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\GetPersonByPapersNumArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\GetPersonByPapersNumArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\GetPersonByPapersNumArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\GetPersonsArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\GetPersonsArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\GetPersonsArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\LoadPersonByWhereArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\LoadPersonByWhereArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\LoadPersonByWhereArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\LoadPersonIdByWhereArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\LoadPersonIdByWhereArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\LoadPersonIdByWhereArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\PersonBean.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\PersonBeanTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\PersonBean.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\Response.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\ResponseTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\Response.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world.model\SavePersonArgs.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world.model\SavePersonArgsTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\SavePersonArgs.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\hello_world\IHelloWorldControllerApi.cs
[main] INFO io.swagger.codegen.DefaultGenerator - File exists. Skipped overwriting J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\hello_world\IHelloWorldControllerApiTests.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\docs\\IHelloWorldControllerApi.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\IApiAccessor.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\Configuration.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\ApiClient.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\ApiException.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\ApiResponse.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\ExceptionFactory.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\SwaggerDateConverter.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\build.bat
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\build.sh
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\\packages.config
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\.travis.yml
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\IReadableConfiguration.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Client\GlobalConfiguration.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\mono_nunit_test.sh
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\\packages.config
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\README.md
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\git_push.sh
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\.gitignore
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\Properties\AssemblyInfo.cs
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\IO.Swagger.sln
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\IO.Swagger.csproj
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger\IO.Swagger.nuspec
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\src\IO.Swagger.Test\IO.Swagger.Test.csproj
[main] INFO io.swagger.codegen.AbstractGenerator - writing file J:\rpcgen\target\hello-world\hello-world-client-csharp\.swagger-codegen\VERSION
```
#### 文件结构
生成CSharp项目代码的文件结构
```
hello-world-client-csharp
│ .gitignore
│ .swagger-codegen-ignore
│ .travis.yml
│ build.bat
│ build.sh
│ git_push.sh
│ IO.Swagger.sln
│ mono_nunit_test.sh
│ README.md
│
├─.swagger-codegen
│ VERSION
│
├─docs
│ CountPersonByWhereArgs.md
│ DeletePersonArgs.md
│ ExistsPersonArgs.md
│ GetPersonArgs.md
│ GetPersonByMobilePhoneArgs.md
│ GetPersonByPapersNumArgs.md
│ GetPersonsArgs.md
│ IHelloWorldControllerApi.md
│ LoadPersonByWhereArgs.md
│ LoadPersonIdByWhereArgs.md
│ PersonBean.md
│ Response.md
│ SavePersonArgs.md
│
└─src
├─IO.Swagger
│ │ IO.Swagger.csproj
│ │ IO.Swagger.nuspec
│ │ packages.config
│ │
│ ├─Client
│ │ ApiClient.cs
│ │ ApiException.cs
│ │ ApiResponse.cs
│ │ Configuration.cs
│ │ ExceptionFactory.cs
│ │ GlobalConfiguration.cs
│ │ IApiAccessor.cs
│ │ IReadableConfiguration.cs
│ │ SwaggerDateConverter.cs
│ │
│ ├─hello_world
│ │ IHelloWorldControllerApi.cs
│ │
│ ├─hello_world.model
│ │ CountPersonByWhereArgs.cs
│ │ DeletePersonArgs.cs
│ │ ExistsPersonArgs.cs
│ │ GetPersonArgs.cs
│ │ GetPersonByMobilePhoneArgs.cs
│ │ GetPersonByPapersNumArgs.cs
│ │ GetPersonsArgs.cs
│ │ LoadPersonByWhereArgs.cs
│ │ LoadPersonIdByWhereArgs.cs
│ │ PersonBean.cs
│ │ Response.cs
│ │ SavePersonArgs.cs
│ │
│ └─Properties
│ AssemblyInfo.cs
│
└─IO.Swagger.Test
│ IO.Swagger.Test.csproj
│ packages.config
│
├─hello_world
│ IHelloWorldControllerApiTests.cs
│
└─hello_world.model
CountPersonByWhereArgsTests.cs
DeletePersonArgsTests.cs
ExistsPersonArgsTests.cs
GetPersonArgsTests.cs
GetPersonByMobilePhoneArgsTests.cs
GetPersonByPapersNumArgsTests.cs
GetPersonsArgsTests.cs
LoadPersonByWhereArgsTests.cs
LoadPersonIdByWhereArgsTests.cs
PersonBeanTests.cs
ResponseTests.cs
SavePersonArgsTests.cs
```
#### 调用示例
参见生成代码文件夹下的README.md