Giter VIP home page Giter VIP logo

截图

drawing drawing drawing

drawing drawing drawing drawing drawing drawing

mobile端

drawing

web端 上面2张图,按通常的开发量需要上千行代码,现在只需要开发10多行代码

我非常赞同的

  • 重复可能是软件中一切邪恶的根源。—— Robert C.Martin

StateGen?

  • 轻代码or敏捷开发? NO;
  • 用可视化界面配置生成? NO
  • 改变原开发模式? NO
  • StateGen不是DEMO,而是一个架构,追求的是易于上手、高效开发、容易维护、可拓展、运行稳定,易于排查的架构

Stategen架构的构成

  • springboot

    • 直接支持 jar war打包模式
  • spring cloud alibaba

    • nacos,seata,sentinel,dubbo,mybatis|ibatis开箱即用
    • 分布式 微服务+本地服务
    • 超动态自动装配消息系统 (spring-stream-3,支持 rocketMq,原则上同时支持kafuka和activeMq)
  • 后端骨架生成器

  • 前端骨架生成器

  • 后端可迭代开发生器(dalgen演化而来dalgenX,全网唯一可支持迭代开发??)

  • 前端开发生成器,可把后端所有任意java api转化为前端的交互代码(mvvm,reactive,react(umi,dva,saga),flutter(provider),依据模版种类)

  • 没有限定使用者集成其它技术

StateGen中的开发生成器,和市面上那些谈虎色变的生成器什么区别?

  1. 后端dalgenx生成器:
    a.从大名鼎鼎的支付宝生成器dalgen演化而来,单dalgen可以说把市面上所有的java orm层生成器秒成渣,dalgenX则在此基础上拓展可迭代功能。dalgen只支持ibatis(个人认为:不开源和不支持mybatis使它推广不开来),而dalgenX则可以在ibatis与mybatis之间自由切换.
    b. dalgenX生成器中的sql相当于Batis sql的用来简化开发的语法糖,它生成可见的纯batis sql和相关java,xml代码,代替肉身做一系列复杂的工作包括查找替换。语法糖不参与运行期,不用提心”国产“框架的坑。
    c.dalgenX生成代码时,会解析已有的java代码,自动增量比对生成,代替肉身备份代码比对还原代码。更方便维护pojo等一系列代码,它也有效治好了DDD模型中的失血模式下的失忆的毛病。
  2. 前端生成器只是在原controller层api方法上加了个java标注 @State而已,对后端代码零侵入,零工作量.
    它成立的理论基础是:
    a. 响应式前端,交互和页面是分开的。
    b. 后端任意一个api,它对应的前端代码:入参、出参对象化,序列化,反序列化,网络调用,状态化都是固定,谁肉身来写都相同的,所以可以用生成器覆盖。
    c. 前端开发生成器只是托管intergrade文件夹下的内容,重点是响应式前端前后端交互部分代码,其它代码只是辅助生成,不再覆盖,使用的同学可无限制优化里面的代码,可以换成自己的理想的前端骨架。
  3. 个人觉得几个生成器可以减少80%的工作,这还不算主要的,主要是底层代码规范,上层代码就不会乱

骨架代码生成流程图

  • 虚线为人工参与点

  • 实线为maven或系统自动装配

  • 粗实线为StateGen自动生成的线路

  • 系统骨架和项目骨架生成器运行时都是幂等,在已有的项目上重新运行只会追加,不会覆盖,不用担心手贱的毛病.

  • 图中"trade"指stategen架构中的系统名 app或cms或xxx指项目

  • 骨架代码生成流程图

  • ​ 直观文件夹树型图: 为什么是多层的?这不是技术范畴而是为了规范和原则,业务系统会做大,层越单纯越安全,通过限定每层pom中引用jar包来限定了每层的功能,比如限定dao只能crud不能出现业务,这种方式比写硬邦邦规章制度好。

  • stg.2.3.1.RELEASE版本可选3种文件夹格式: numHeader(默认):1-trade-pojo;numMid:trade-1-pojo; numNone:trade-pojo,在ide中排序后人眼对系统结构顺序一目了然(有序依赖的业务系统在ide内有序排列工作效率会高)

trade (trade相当于微服务中当前服务名、系统名)
├── 1-trade-pojo		//失血或贫血模式,无复杂业务逻辑,不暴露隐私业务逻辑。
├── 2-trade-facade		//和1-trade-pojo组成对外jar包,越纯洁越好
├── 2-trade-sink-facade		//消息订阅方jar,别人按你的格式发送消息,Provider不需要配置,配置自动装配
├── 3-trade-intergrade		//单纯引用别的系统中的jar,不依赖于本系统中其它jar,方便做jar冲突排查
├── 4-trade-dao			//限定为crud,无业务逻辑
├── 5-trade-service      //业务逻辑实现部分
├── 6-trade-web-base     //业务逻辑实现部分+和共用项目controller,让后面的7遵从每个数据库只有一套curd服务的原则。
├── 7-trade-web-app      //springboot application,引用 6-trade-web-base和实现自身独特业务逻辑
│   ├── app-frontend-flutter
│   ├── app-frontend-h5
│   └── WebRoot           
├── 7-trade-web-cms		//springboot application,引用 6-trade-web-base和实现自身独特业务逻辑
│   ├── cms-frontend-web
│   └── WebRoot
├── 7-trade-web-...		//springboot application,引用 6-trade-web-base和实现自身独特业务逻辑
├── opt
│   └── config
│       └── stategen
└── tables

骨架快速开始

运行环境

服务端/windows(linux类似)

A. java 1.8+
B. maven 3 (3.5.0有bug,请使用3.5.2+) C. mysql5.7
D. gitbash(安装git2.0 自带,目的是可在windows上执行bash脚本)
E. nacos-server-1.3.2 (因为目前架构中用到的spring cloud alibaba denpencies版本为2.2.3,其中限定nacos client为1.3.3,它与nacos1.4.0-server通信有障碍,本架构用于生产,不在尝鲜版上纠缠,等他们稳定了再升级) F. sentinel dashboard-1.8.0 ps:因为在dashboard上操作不能反向持久化到nacos中,开发和生产很不方便,有大神制从原版中修改了代码制了nacos反向持久化版,我稍微忧化可用性和方便性,,https://github.com/stategen/sentinel-dashboard-nacos 它的启动方式是这样的:

 java -Dnacos.server-addr=localhost:8848 -Dserver.port=8880 -Dcsp.sentinel.dashboard.server=localhost:8880 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-nacos-1.8.0.jar

G. seata-server-1.4.0

开发环境安装

因项目依赖jars我已经发布maven**仓库了,不需要使用的同学再辛苦自行编译,只需要git clone dalgenX,这是一个GMAVEN项目,无需打包

  1. 配置 dalgenX (ps:dalgenX并不依赖于相StateGen架构,它可以配到其它spring项目中单独生成前后端代码),
git clone https://github.com/stategen/dalgenx.git
(把 gen.custom.xml.templete 复制一份为 把 gen.custom.xml)
  1. 设置 DALGENX_HOME 环境变量为 dalgenx所在目录

  2. 将 %DALGENX_HOME% 添加至 PATH 中

  3. Ide中配置(eclipse|myeclipse|idea)xml文件,方便开发时打字提示.

location: {DALGENX_HOME}\gen.schemas-1.0.dtd
key type: system Id
key: https://github.com/stategen/dalgenx/blob/master/gen.schemas-1.0.dtd

用命令初始化系统及项目/范例

ps: 以下gen.sh 必须在gitbash中运行,不能在cmd中运行。linux可以无需考虑。

  1. 帮助
gen.sh -h
  1. 创建系统骨架,注意,骨架|脚手架生成器操作是幂等的,可以多次执行. -e 是当有错误时,输出错误信息.
gen.sh system com.mycompany.biz trade -e  

com.mycompany.biz 为包名
trade 系统名 /数据库名 dubbo 系统名
至些,一个StateGen系统分分中创建完成,接下来添加项目,可以无限添加项目,为啥这样搞?它们遵从同一个数据库只有一套crud服务的原则.减少扯皮,减少在泡在会议室的时间,谁改完了表,都得改想关的影响点,,好在,因为架构上它们在同一个系统中,ide会帮你及时发现兼容bug

  1. 创建app web api 项目 (可选) , (不想参与前端的同学,涉及到前端的命令参数都可以忽略
//创建后端项目时,同时创建前端,前端目前有3个模版 h5,web,flutter 
gen.sh project app h5 –e  //带前端
//也可以:
gen.sh project app –e  //不带前端
//**h5和web对应的前端是nodojs项目,ssr模式,因此打包时,maven插件会编译前端**
//**必须安装nojs和yarn才能编译通过,也可到7-tradeApp的pom.xml中,把编译注释掉**

  
//因为app项目上面已创建,生成器会判断,以下命令只创建了一个flutter前端
gen.sh project app flutter –e  

//或者时入子目录
cd 7-trade-web-app
gen.sh client flutter -e //这样也可以创建flutter前端
  1. 创建cms web 项目 cms指后台管理系统, (可选,也可以以后再创建或不要)
gen.sh project cms web –e 
也可以 
gen.sh project cms –e 
  1. 创建shedule项目,不带前端, (可选,也可以以后再创建或不要)
gen.sh project schedule –e  //无前端,可以跑定时任务

//变为git版本控制, trade 为git 项目,app-frontend-flutter为trade的子项目,(git子项目其实也是一个独立的git项目,我个人觉得git这点比svn理念先进).

sh gitinit.sh //变为git项目
cd app-frontend-flutter
sh git_add_to_parent_as_sub.sh //变为trade 的git子项目
  1. 环境及表

创建trade数据库并运行 运行 trade.sql 也可以建一个空表. 把opt复制到同盘(tomcat所以盘或者你的ide同盘,这样目的是保持开发和运行环境一致。windows下/opt指的容器同盘目录)根目录下,修改stategen.xml中的数据库配置,只需要关注mysql,nacos,相关配置,zookeeper和redis可以不用管 7-xxx下的stategen.xml相关的内容合并到/opt/config/stategen/stategen.xml中 修改gen_config.xml中的开发数据库配置
因为一些文件可以用生成器获取,所以不在版本控制里,先后在 gitbatsh中运行 tablebatch.sh 和 dalbatch.sh ,空表则不需要运行.

sh ./tablebatch.sh 
sh ./dalbatch.sh 

启动TradeAppApplication.java 每个7中都有一个TradeXxxApplication.java

启动成功后控制台可以看到以下信息:

	Application  is running! Access URLs:
	servletWebServerFactory  类型	:UndertowServletWebServerFactory:
	Local访问网址: 		http://localhost:8080/tradeApp
	应用访问网址: 		http://192.168.112.1:8080/tradeApp
	Swagger网址: 		http://192.168.112.1:8080/tradeApp/doc/index.html

ps: 1.StateGen生成的架构的代码,启动时对端口做了占用检查,因此:一个项目启动多个实例也是没问题(只要不启动太快),如8080端口,它的配置是这样的

    <bean class="org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory">
        <property name="contextPath" value="/${spring.application.name}" />
        <!-- 逻辑:如果端口被占用,则往上找直到没有被占用的端口 -->
        <property name="port" value="#{T(util.Net).from(${tradeApp.port:8080})} " />
    </bean>

ps: 2. StateGen默认你的项目会越来越大(谁个系统还没有百把张表或者自定义实现spring系统bean),因此:除了必要的bean是代码创建的外,都是xml配置的,大系统,xml才是王道 .

在nacos上应该可以看到dubbo服务:

nacos_trade截图

在swagger中 调用一下一个AppController.java中的 testSentinel api

    /***测试限流降级分布式事务*/
    @ApiRequestMappingAutoWithMethodName(method = RequestMethod.GET)
    @SentinelResource()
    public User testSentinel(@ApiParam(value="用户ID",defaultValue="1") @RequestParam() String  userId) {
        //MockUtil只能用于测试,不能打包,执行 mvn package 由 插件 forbiddenapis 检测
        MockUtil.throwRandomException(2);
        User user = this.userService.getUserByUserId(userId);
        return user;
    }
    

到Sentinel-dashboard中可以看到

sentinel-trade

设置限流降级,单机阈值设为2,快速在swagger中调用testSentinel,可以看到如下限流返回值

{
  "code": 500,
  "exeptionClass": "FlowException",
  "message": "该阶段不支持该操作(限流),请稍后再试",
  "status": "ERROR",
  "success": false
}

上面的message信息可以配置的:

    <bean class="org.stategen.framework.spring.mvc.SentinelBlockHandler">
        <property name="blockResponseStatus">
            <util:constant static-field="com.mycompany.biz.enums.ResponseStatus.BLOCK" />
        </property>
        <property name="msgFlowException" value="该阶段不支持该操作(限流),请稍后再试"/>
    </bean>
  1. 微服务集群相互调用:

    按上面我们再创建一个系统:verify ,(另一个文件夹),

//创建系统
gen.sh system com.mycompany.verify auth -e
//创建系统中的项目
gen.sh project microServ -e

mavan打包发到公司私有仓库或者安装到本地仓库:

mvn install|deploy

在原来的trade系统中3-trade-intergrade中pom.xml里面,只要引用即可。

<dependency>
    <groupId>com.mycompany.verify</groupId>
    <artifactId>auth-facade</artifactId>
    <!-- verify 系统每次变更,要改 verfy-facade.version版本号-->
    <version>1.0.0</version>
</dependency>

10 . 一个典型Stategen 系统 结构图

Image

迭代开发流程图

  • 虚线为人工代码参与点

  • 实线为maven或系统自动装配

  • 粗实线为dalgenX自动生成和迭代的线路

  • 从流行程上来看,

    • dalgenX没有改变原开发模式
    • 保留之前工作代码成果,新生成代码增量添加
    • 人工编写的代码可以做到仅限业务

迭代开发流程图

演示User需求开发 (一键开发,一键迭代,显式代码,所见即所得)

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `teacher_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户ID',
  `teacher_name` varchar(64) DEFAULT NULL COMMENT '用户名',
  `password` varchar(64) DEFAULT NULL COMMENT '密码,测试,明文',
  `role_type` varchar(32) DEFAULT NULL COMMENT '用户角色 ADMIN,DEFAULT,DEVELOPER',
  `name` varchar(64) DEFAULT NULL COMMENT '姓名',
  `nickName` varchar(32) DEFAULT NULL COMMENT '别名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `address` varchar(255) DEFAULT NULL COMMENT '详细地址',
  `avatar_img_id` varchar(64) DEFAULT NULL COMMENT '头像 ID',
  `email` varchar(128) DEFAULT NULL COMMENT '邮箱',
  `vali_datetime` datetime DEFAULT NULL COMMENT '认证时间',
  `birthday_date` date DEFAULT NULL COMMENT '出生日期',
  `work_time` time DEFAULT NULL COMMENT '工作时间',
  `province_id` varchar(64) DEFAULT NULL COMMENT '省份 ID',
  `city_id` varchar(64) DEFAULT NULL COMMENT '城市 ID',
  `status` varchar(64) DEFAULT NULL COMMENT '状态 enum',
  `grade` bigint(2) DEFAULT NULL COMMENT '级别',
  `sex` tinyint(1) DEFAULT NULL COMMENT '性别',
  `post_address_id` bigint(20) DEFAULT NULL COMMENT '邮寄地址 ID',
  `remark` text,
  `create_time` datetime(6) DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime(6) DEFAULT NULL COMMENT '更新时间',
  `delete_flag` int(1) DEFAULT NULL COMMENT '是否删除 (0:正常,1删除)',
  PRIMARY KEY (`teacher_id`),
  UNIQUE KEY `email` (`email`),
  UNIQUE KEY `teacher_name` (`teacher_name`),
  KEY `province_id` (`province_id`),
  KEY `city_id` (`city_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

1.运行命令生成表sql配置和java代码 (或者在6-${systemName}-web-base/test下运行 DevGennerator.java)

gen.sh table user –e

2.检查teacher.xml对应的java类是否正确,去掉?及一行空格

gen.sh dal user –e

服务的继承关系为 : UserServiceImpl implements <<UserService extends <<UserServiceTrade UserService内的服务都是本地服务,复制接口到 UserServiceTrade中,即可暴露微服务 我个人觉得dubbo还有一个开发优势是微服务异常也可以原文传递

F5刷新eclipse 检查import是否完整
动做一个controller或者用命令初始化一个controller

//只是辅助快速生成一个UserController.java,一但生成后,每二次执行不会覆盖
gen.sh api user cms|app

分布式事务Seata已经集成到里面了,使用的地方禁注一下@GlobalTransactional.,如:

    /***测试seata分布式事务*/
    @ApiRequestMappingAutoWithMethodName(method = RequestMethod.GET)
    @GlobalTransactional
    public User testSeata() {
        User user = this.userService.appendUserAge("2");
        return user;
    }

分布式id生成器baidu uid也集成在里面,不用再像以前一样往redis或zookeeper内肉身放置

public class UserServiceImpl implements UserService, IdGenerateService<String> {

    @Resource
    private IIdGenerator idGenerator;

    @Override
    public User insert(User user) {
        return userDao.insert(user, this);
    }
end    
  1. eclipse打开查看是否有代码错误

生成前端代码,开发前端(不做前端可以暂时不用看)

  1. 运行test/UmiFacadeProcessor.java
  2. webstorm打开前端代码 ,配置webpack解读代码
  3. yarn 下载前端依赖
  4. fiddler 脚本设置 onBeforeRequest 函数中
        var url:String=oSession.PathAndQuery;
      
        if (oSession.host=="localhost:8000") {         
            if ( url.StartsWith("/tradeCms/api/") || url.StartsWith("/tradeCms/uploads/") ) {
                oSession.host="localhost:8080";
            }            
        } 
        else if (oSession.host=="localhost:8001") {
            if ( url.StartsWith("/tradeApp/api/") || url.StartsWith("/tradeApp/uploads/") ) {
                oSession.host="localhost:8080";
            }            
        }
  1. 后端发布到eclipse tomcat中,运行
  2. yarn run dev
  3. 后端代码变化后, 直接运行对应的 XXXFacadeProcessor.java,前端可实时开发编译

帮助

stategen对服务端代码的增强介绍

Stategen要做的事,尽量地合理实现一个商业框架(不是开源后阉割版的那种)。一些过时的技术比如osgi摈弃,尽量在spring技术范围内解决。一些拓展点技术(非spring)我个人觉得对业务代码没有帮助而是挖坑(大牛一走,项目搞不下去了)

  1. @Wrap对返回置封装,对业务代码零侵入
        //以前的代码是这样地恶心
        @SuppressWarnings("unchecked")
        @ResponseBody
        @RequestMapping("getUserByUserId")
        public Response<User> getUserByUserId(String userId){
            User user = this.userService.getUserByUserId(userId);
            if (user!=null) {
               return new Response<User>(user);
            } else {
                return  (Response<User>) Response.error("用户不存在"); 
            }
        }

上面的java代码里到处对返回值封装,现在可以通过以下方式让spring自动封装返回值

    <!-- Response可以自定义,也即自定义封装 -->
    <bean id="response" class="com.mycompany.biz.domain.Response" 
    scope="prototype"/>
    //@warp也可以一次性配在Controller上
    @Wrap
    public class UserController{
        //现在
        @ResponseBody
        @RequestMapping("getUserByUserId")
        //@warp也可以一次性配在Controller上
        @Wrap
        public User getUserByUserId(String userId){
            User user = this.userService.getUserByUserId(userId);
            BusinessAssert.mustNotNull(user, "用户不存在");
            return user;
        }
    }
    
 /*api返回值,自定义实现Response.java类就可以自定义实现封装*/
{
    message:'成功',
    success:true,
    data:{username:'张三',nickname:'zhangsan',...},
    ...
}
        //当@wrap一次性配到controller上时,也可以把个别api除外
        @Wrap(false)
        @ResponseBody
        @RequestMapping("deleteUserById")
        public String deleteUserById(String userId){
            this.userService.delete(userId);
            return userId;
        }
  1. @ApiRequestMappingAutoWithMethodName 对 @RequestMapping硬编码的处理
    @ResponseBody
    /*这里有硬编码而且允许与methodname不一致,review时,看到里而全是xxxNew,xxxOld,xxxV1,xxxV2
    真有想打人的冲动。
    再比如,当前端反馈getUser有问题,
    后端还要搜一下代码才能定位,代码交接和team衔接时先得跳坑 */
    @RequestMapping("getUser") 
    public User getUserByUserId(String userId){
        User user = this.userService.getUserByUserId(userId);
        return user;
    }
    /*现在直接下面标注,它等于@RequestMapping("getUserByUserId"),
    但是不用写硬编码,直接跟方法名走,同时有swagger2::@ApiOperation,@@ResponseBody*/
    @ApiRequestMappingAutoWithMethodName
    public User getUserByUserId(String userId){
        User user = this.userService.getUserByUserId(userId);
        return user;
    }
  1. 统一错误处理
    //以前是这样地恶心
    //@...省略
    public Object getUserByUserId(String userId){
        try {
            User user = this.userService.getUserByUserId(userId);
            return user;
        } catch (Exception e) {
            logger.error("", e);
            return  Response.error("服务端异常"); 
        }
    }
    <!-- 现在只需要配置一个bean ,在骨架xml中, 这里只是讲解,不需要再添加-->
    <bean class="org.stategen.framework.spring.mvc.CollectExceptionJsonHandler">
        <!-- 注意这里用Class可以避免硬编码,我真的讨厌硬编码,哈哈-->
        <property name="responseStatusClzOfException" 
            value="com.mycompany.biz.enums.ResponseStatus.ERROR" />
    </bean>
    //然后,就不用关注异常和错误日志输出了,函数返回值也限定为User,
    //异常和日志由CollectExceptionJsonHandler自动处理,错误也自动包装返回给前端
    //@...省略
    public User getUserByUserId(String userId){
        User user = this.userService.getUserByUserId(userId);
        return user;
    }

4.方法鉴权和垂直权限

    //以前是这样的校验,除了恶心之外,token很容易被截获,api被人破了后,得有人背锅跑路
    //@...省略
    public Object getUserByUserId(String userId,String token){
        if (processToken(token)) {
            return Response.error("没有登录")!
        }

        User user = this.userService.getUserByUserId(userId);
        return user;
    }
    
    <!--现在我们可以配一个bean统一处理方法鉴权和垂直权限  ,在骨架xml中, 这里只是讲解,不需要再添加 -->
    <bean id="authCheckerHandlerInterceptor"
        class="org.stategen.framework.spring.mvc.AuthCheckerHandlerInterceptor">
        <!-- 用类不用字符串硬编码 -->
        <property name="responseStatusClzOfCheckFailDefault"
            value="com.mycompany.biz.enums.ResponseStatus.NOT_LOGIN" />
    </bean>
    
    <bean class="com.mycompany.biz.checker.VisitChecker" />
    <bean class="com.mycompany.biz.checker.LoginChecker" />
    //以前是这样的校验,token很容易被人截获,架构师背锅跑路
    //@...省略
    /*也可以配到Controller上统一处理,由于token不再时单一值,
    我们可以对token进行先进行防伪造签名校验..., @LoginCheck(exclude=true)例外
    */
    @LoginCheck 
    @VisiterCheck
    ...想验证哪个实现哪个Checker
    //返回值是具体的类,比如User
    public User getUserByUserId(String userId){
        User user = this.userService.getUserByUserId(userId);
        return user;
    }
  1. Cookie校验.我刚写框架改造我们那个旧系统(每个url后都有个?token=xxxxxx,除了恶心就是不安全,hibernate还把token返回给所有用户,怕怕),要用到Cookie,我的CTO(真来自国际大厂)反对说:Cookie不安全不能用,我对他打了个比方,门不安全不等于连门都不设让小偷直接进来,我们要想法改造门让它变安全。实际上,cookie作为http协议的一部分,无论是服务端或者客户端都非常成熟的实现,是会不会用的问题,活用cookie可以减少服务端和客户端非常大的工作量。浏览器打开大厂淘宝的cookies看看,它有一个cookie名叫_tb_token_,这个cookie是taobao对其站内其它cookie的签名。
    stategen中的cookieGroup就是对_tb_token_的开源实现,支持混淆码由运维控制。考虑到cookie的多样性,cookie多的情况下,也不易控制,特意给Cookie做了分组,
    以上只是讲把一些值放在cookie里,当然放在head里也可以(放head里生命周期要自己管理),但最好不要拼在url后面或form里
    <!-- 验证cookie分组 ,该类可以多个,配置不同的分组 -->
    <bean id="loginCookieGroup" class="org.stategen.framework.web.cookie.CookieGroup">
        <property name="cookieTypeClz" value="com.mycompany.biz.enums.CookieType.Login" />
        <property name="httpOnly" value="${loginCookieGroupHttpOnly}"/>
    </bean>
    /*代码中这样注入,支持String或枚举拿放cookie,又枚举?哈哈,
    我是真的不喜欢字符串硬编码。系统中,cookie名称是有限的,枚举远比static String更好地限定数据范围,*/
    @Resource
    private CookieGroup<LoginCookieNames> loginCookieGroup;
    
    //放置简单,直接放,签名是由CookieGroup内部自动完成的
    loginCookieGroup.addCookie(LoginCookieNames.userId, loginUser.getUserId());
    
    /*cookie值是有签名的(xx_token),客户端无法伪造这个值。开发人员也不能反算这个值,
      因为混淆码读取stategen.xml中的,由运维控制的*/
    String userId = this.loginCookieGroup.getCookieValue(LoginCookieNames.userId);

需要说明的是:Cookie校验是在filter中进行的。那为啥不在springMVC中呢?打个比方,Cookie校验是防伪造校验,好比总入口大门的保安一眼就能识别来人是否合法,就没必要先搬来各种重型设备再一眼就能识别是否合法,对系统资源利用上的浪费

    //spring web3.0
    @WebFilter(filterName = "CustomMultiFilter", urlPatterns = "/*")
    public static class CustomMultiFilter extends org.stategen.framework.spring.mvc.MultiFilter {
    }
  1. 环境配置与打包无关。环境配置是运维的冬冬,应该由运维来控制,还有一些是敏感数据,比如数据库密码,这些是万万不能给到开发人员的,网上远程删库跑路的悲剧又不是一回两回了,但常用的maven spring打包都不能避免这种坑,我是亲眼到我上一任架构师打个包像做贼一样,打完包还担心得要死(怕环境搞错了)就这么小心还是犯疏忽。stategen把环境变量和日志配置都放到/opt/config/stategen/,由运维控制,一劳永逸,同时支持windows上开发,linux运行,测试通过war还可以直接由手工或jekkins直接扔生产,而不用再打包,避免风险。这期间,开发、测式、运维和气生财。大厂antx.xml也这么处理的,不是我独创
    <bean id="propertyPlaceholder"
        class="org.stategen.framework.spring.mvc.MultiPropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath*:application.properties</value>
                <value>file://opt/config/stategen/stategen.xml</value>
            </list>
        </property>
    </bean>
    <!-- stg.2.3.0.RELEASE配在bootstrap.yml中 -->
    #application.properties
    logback.configfile.xml=file://opt/config/stategen/logback-config.xml
  1. 支持分布式id生成器,目前默认是百度uid-generator,非强制性
    <!-- 启用baidu uid ,这里可以看出大厂也喜欢用xml显式配置bean,为啥,自己体会 -->
    <import resource="classpath*:uid/cached-uid-spring.xml" />
    <bean id="idGenerator" class="com.mycompany.biz.service.impl.IdGeneratorImpl"/>
  1. 开关注册 dubbo provider服务。默认是开启的,但有些小系统比如跑定时任务的,它只是微服务的消费者可以不是生产者,这种配置可以做到,配在application.yml则不行.
    <!-- 反注释 dubbo-provider-spring.xml 中的 -->
    <!-- <import resource="classpath*:context/dubbo-provider-auto-*.xml" /> -->    
    <!-- <import resource="classpath*:context/dubbo-provider-manual-*.xml" /> -->
  1. 国际化...以后再讲,我觉得也很屌

  2. dalgenX生成器支持在ibatis和mybatis语言两种orm之间切换:

    <!-- gen_config.xml中 -->
    <!-- ibatis,mybatis,最下面覆盖上面,最下面优先 ,修改顺序后,需要重新运行一次 ./dalbatch.sh 批量生成-->
    <entry key="dao_type">mybatis</entry>
    <entry key="dao_type">ibatis</entry>
  1. dalgenx借鉴了dart和typescript等先进语言, 有几个语法糖如下,可以减少where条件书写工作量,使sql简洁易懂(注:语法糖会被编译成最终sql,不参与运行期)
    <!-- user.xml中一条方法,这个是自动生成的,如果手工书写时,有提示帮助-->
    <!-- a.username 不需要写 小驼峰名称,以及jdbctype参数,
    不需要写返回值对照配置,由dalgenX生成User-sqlmap-mapping.xml自动生成 -->
    <operation name="..." remarks="">
        <sql>
            select
            a.user_id,
            a.username,
            a.name,
            a.nickName,
            a.code,
            a.grade,
            a.status
            from user a
            where
            a.delete_flag = 0
            and a.username=? /*必选条件,省略参数名*/
            and a.name like #cstmName# /*必选条件*/
            and a.username=?? /*动态条件,省略参数名*/
            and a.nickName=?#nkName# /*动态条件*/
            and a.code > ? /*必选条件,省略参数名*/
            and a.grade &lt; #gradeList# /*必选条件*/
            and a.grade in ?? /*动态条件,省略参数名*/
            and a.status not in ?#statusList#  /*动态条件*/
        </sql>

    </operation>
  1. in语法糖:
 a.mobile in ?

 a.mobile in #mobiles#

会被生成如下:
mybatis2|ibatis: (若在gen_config.xml中配置 List,则mobiles-->mobileList,否则复数形式按常用英文规则推导.
mybatis2|ibatis: )

    a.mobile in
    <iterate property="mobiles" conjunction="," open="(" close=")">
        #mobiles[]#
    </iterate>

mybatis3:

    a.mobile in 
    <foreach collection="mobiles" item="item" separator="," close=")" open="(">
        #{item}
    </foreach>
  1. ?? 或 ?#paramName# 语法糖:
 and a.address =??

 and a.address =?#address#

等同于以下,其中string类型或list类型用isNotEmpty标签封装 ,其它用isNotNull, 条件符支持and 、or,操作符支持 =、!=、<>、>=、<=、like、not like、in、not in,
mybatis2|ibatis:

    <isNotEmpty property="address" prepend="and">
        a.address =#address#
    </isNotEmpty>

mybatis3: (@util.Check@isNotEmpty可以gen_config.xml自由配置其它判空函数)

    <if test="address != null and @util.Check@isNotEmpty(address)">
        and a.address =#{address}
    </if>
  1. 以上2个合在一起写也可以即 in ??和 in ?#listParamName#,如:
 and a.mobile in ??

 and a.mobile in ?#mobiles#

等同于以下形式:
mybatis2|ibatis:

<isNotEmpty property="mobiles" prepend="and">
    a.mobile in 
    <iterate property="mobiles" conjunction="," open="(" close=")">
        #mobiles[]#
    </iterate>
</isNotEmpty>

mybatis3:

    <if test="mobiles != null and @util.Check@isNotEmpty(mobiles)">
        and a.mobile in 
        <foreach collection="mobiles" item="item" separator="," close=")" open="(">
            #{item}
        </foreach>
    </if>
  1. dalgenX生成mybatis文件时,也完整地实现mapper/daoImpl,襾不是采用mybatis的java代理方式,原因2个,反正生成的代码不需要维护,显式代码执行速度比代理快,2,显式代码调试跟踪断点日志方便。
public class UserDaoImpl  extends SqlDaoSupportBase implements UserDao {
	/**
	 * sql:...略
	 * a.username 对应的参数自动生成小驼峰名称,以及参数类型,函数返囲值 
	 */
	public User getUserByUsername(String username) throws DataAccessException {
	    //HashMap初始化时,大小都自动确定了,节约内存,提高效率。
		Map<String,Object> params = new HashMap<String,Object>(1);
		params.put("username",username);
		/*下面User.getUserByUsername自动插入到生成的sql中 
		select /*User.getUserByUsername*/ ... from ...,方便druid中跟踪sql的执行效率,巴结DBA,哈哈
		*/
		return (User)super.selectOne("User.getUserByUsername",params);
	}
	...
dalgenx将水平权限(数据权限)融入架构中解决
  1. 生成规则。水平权限要完全做到绕开暴力尝试,或者避免在别的api中泄露id被利用,显然,采用复杂id(uid、随机)生成方式治标不治本。同时,要兼顾代码速度、迭代、人员权限调整、下面简要地阐述一种水平权限方案,可以直接由dalgenX生成器来生成,大大降低开发成本,非常适合产品需求上的迭代,代码可以做到以不变应万变.
   A.定义一个组织架构表比如orgnization,树型数据 orgId, parentId
   B.把用户(即水平权限中的数据操作员)人分配到组织上(org_user表),
     用户登录后获取自己的orgId即currOrgId,
   C.假设topic表需要水平权限控制,在表的备注中添加 -level(organization) -owner(user) 
      ,让dalgenX识别。
      -level(organization) //水平权限中的组织架构表为organization 
      -owner(user) //水平权限中数据属于指定人员表为user
   D.运行 gen.sh table topic时,会生成 topic_level_h 和 topic_owner_h表创建sql语句,复制出来运行。
   (dalgenX约定后缀为"_h"为水平权限相关的表)   
   E.用户生产数据时(比如topic表),同时把数据添加到
     topic_level_h和topic_owner_h(由dalgenX显式生成相关的sql和调用java代码)
   F.用户查询,删除,更新数据时,由dalgenX显式生成相关的sql和调用java代码
     和参数:Boolean inclCurrOrgId, Long currOrgId, String currUserId
   G.由程序员在调用topicService的方法时,自由控制inclCurrOrgId,currOrgId,currUserId
dalgenX兼顾避免一些开发习惯上的坑。比如
   /*根据主键查询,一般dao中的方法名是getById,
   我们在很多遗留的代码中,经常review到下面这样的代码,,
   这是开发调用ide自动代码完成功能后,没有改变量名 */
   User byId = userService.getById(id);
   ...
   ...
   byId.method1();
   byId.method2();
   byId.method3();
   //上面的byId没法扯皮,吵不过人家
   
   /*dalgenX根据主键查询,dao中的方法名是getUserById,
   简定开发忘记不改变量名,ide自动完成的变量名: */
   User userById = userService.getById(id); 
   ...
   /*好吧,无论什么时候阅读到下面的代码,
   看到userById也知道其类型是User */
   userById.method1();
   userById.method2();
   userById.method3();   
dalgenX在Service中生成一些符合合成/复用原则(CARP)的java代码,这种开发规范好理解也节约业务层大量开发
public class UserServiceImpl implements UserService {
    
    //收集Bean上的userId,把查询到的user赋值到Bean的User上
    public <D> void assignBeanTo(Collection<D> dests, Function<? super D, String>
        destGetMethod, BiConsumer<D, User> destSetMethod) {
        ...
    }
    //收集Bean上的userIds,把查询到的users赋值到Bean的List<User>上
    public <D, G> void assignBeansTo(Collection<D> dests, Function<? super D, G>
        destGetMethod, BiConsumer<D, List<User>> destSetMethod, 
        BiConsumer<User, List<G>> resultSetQueryIdsFun, Function<? super User, G> resultGetGoupIdFun) {
        ...
    }

    //收到UserId,比如合并到List<Teacher>要
    public <D> void mergeBeanTo(Collection<D> dests, Function<? super D, String>
        destGetMethod) {
        ...
    }
    ...
}

   //调用非常简单,没有循环处理,没有硬编码码,如给List<Topic>每条Topic赋值作者信息
   userService.assignBeanTo(topics, Topic::getAuthorId, Topic::setAuthor);

四 、前端生成器

  1. 生成器适合响应式前端,不是旧式的mvc的jsp,jquery,easyui或者类似的冬冬.
  2. 对于后端一个任意给定的api,其对应的前端网络调用、数据状态化、交互代码基本都是确定和没有歧义的,既然是确定的,说明是规有律性的,找到规律就可以实现机器来生成, stategen在不增加学习和额外开发成本的情况下找到了这种规律,它避免了以往手工或半手工导致的不规范而增加开发、维护成本。现在stategen可以自动秒撸
  3. 经过分析,同一作用域中,任何新调用的api返回值与之前的数据之间的关系只有以下3种,如此,前端状态的自动化代码有了理论基础:

a. 重新加载
b. 按主键增加或更新
c. 按主键删除

  1. 数据隔离
    A. 按后端返回值类型隔离,同一个后端controller对应前端的model/provider,以下都是按 userArea 隔离
    User,
    List<User>
    PageList<User>,
    @State(area=User.class) public String delete(){}
    B. 不同的Controller数据已经按model/provider隔离
  2. 以前生成代码方式有2种,配置和伪代码:这两种方式增加学习成本,看似节约时间,实际上要花费更多的时间在坑里维护代码,任何基于json或者配置做前端都是太菜了,stategen不这样做. StateGen直接通过java反射硬解析后端java代码来生成前端代码,没有任何多余工作环节或学习,不改变开发流程.当然也不存在挖坑
  1. 对于任意一个sql,其配置、对应的java代码,参数个数,类型,返回值类型、字段都是确定的,这些以前都是手工或半手工撸出来.
  2. 业务逻辑都是由调用一个或多个sql组成的
  3. 市面上代码生成器都覆盖式生成代码,其生成的代码无法预先指定继承、实现接口、类型指定,无法保护已有业务代码成果, 比如要多写功能相同逻辑的DTO绕开限制,但这违背单一职责框架设置原理,欲使用先入坑.StateGen采用解析已有java 代码的方式解决上述问题

代码生成器难以解决的是问题迭代和增量开发,但是实际的项目开发都是不断地迭代功能和新功能叠加,而stategen非常适合迭代开发

StateGen已经支持flutter

采用google 2019 i/o大会上推荐的provider ,具体看demo中演示

以下以typescript和dart为例(StateGen采用freemark模版,也可以生成其它语言样式,懂rxSwift或rxAndroid的朋友也可以协助制作相关代码). 特别说明,Stategen前端代码生成器,主要功能是通过对前端integrade文件夹的自动托管,让前端的工作量尽量集中在排版和美工上,前端龙骨代码是一次性的,程序员可以自行修改(骨架代码并非我擅长,再说再好的骨架代码也不是永久都是最好,所以没有写死,也不把前端朋友限定死,原则就是不挖坑)。
以下代码都是在integrade文件夹内.

//typescript
export default class TopicApis {

  /**
   * POST /api/topic/getTopicPageList
   */
  static getTopicPageList(params: { topicType?: TopicType, mdrender?: boolean, 
    page?: number, pageSize?: number }): AntdPageList<Topic> {
    let requestInit: RequestInitEx = <RequestInitEx>{};
    //tradeAppBaseUrlKey相当于http://domain,这里不写死,方便配置和用fiddler测试
    requestInit.apiUrlKey = tradeAppBaseUrlKey;
    //url永远都跟着后端走,别的团队出文件,咱直接出代码,准确,下同
    requestInit.url = '/api/topic/getTopicPageList';
    requestInit.mediaType = MediaType.FORM;
    requestInit.data = params;
    requestInit.method = Method.POST;
    return Net.fetch(requestInit);
  }
  ...
}
///dart语言
class TopicApis {
  /// POST /api/topic/getTopicPageList
  /// 
  static Future<AntdPageList<Topic>> getTopicPageList({Map<String, dynamic> payload,
    TopicType topicType, bool mdrender, int page, int pageSize }) async {
    var requestInit = RequestInit();
     //tradeAppBaseUrlKey相当于http://domain,这里不写死,方便配置和用fiddler测试
    requestInit.apiUrlKey = tradeAppBaseUrlKey;
    //url永远都跟着后端走,别的团队出文件,咱直接出代码,准确,下同
    requestInit.path = '/api/topic/getTopicPageList';
    requestInit.mediaType = MediaType.FORM;
    payload ??= {};
    if (topicType != null) {
      payload['topicType'] = topicType;
    }
    if (mdrender != null) {
      payload['mdrender'] = mdrender;
    }
    requestInit.data = payload;
    requestInit.method = Method.POST;
    var dest = await NetUtil.fetch(requestInit);
    return AntdPageList.fromJson(dest, Topic.fromJsonList);
  }
  ...
}

包括以上api所依赖的bean,enum,泛型同时生成
对于flutter,支持多云序列化、反序列化,比flutter插件准备,节省时间,快速迭代

///dart语言
class Topic with FrontBean {
  /// topicId
  static const String Topic_ID = 'topicId';

  /// 主题ID
  String topicId;

  /// author
  User author;
  //... 
  static Topic fromJson(Map<String, dynamic> json) {
    if (json == null) {
      return null;
    }
    return Topic(
      //多层自动调用反序列化
      author: User.fromJson(json['author']),
      //基本类型采用依赖倒置原则(DIP),不留搞,不挖坑
      authorId: JsonUtil.parseString(json['authorId']),
      //...
    );
  }  
  
  @override
  Map<String, dynamic> toJson() {
    var result = new Map<String, dynamic>();
    if (this.author != null) {
      //多层自动调用序列化
      result['author'] = author.toJson();
    }

    if (this.topicId != null) {
      //基本类型采用依赖倒置原则(DIP),不留搞,不挖坑
      result['topicId'] = JsonUtil.stringToJson(topicId);
    }
    //...
}

/*对于dart语言,它的枚举值实际对应的是数字,表示后端enum值没有意义,
不知道写dart的人脑袋是不是进水了,没有java一样的枚举算什么快速开发?
stategen也完美地避开这个坑 */
class TopicType extends ClassAsEnum<TopicType> {
  const TopicType(value, title) : super(value, title);

  /// 精华
  static const good = TopicType("good", '精华');

  /// 分享
  static const share = TopicType("share", '分享');
  //...
  static Map<String, TopicType> _map = {
    good.value: good,
    share.value: share,
     //...
  };

  static TopicType fromJson(dynamic value) {
    return _map[value];
  }

  static List<TopicType> fromJsonList(List<dynamic> values){
    return JsonUtil.parseList(values, TopicType.fromJson);
  }

  static Map<String, Option> topicTypeOptions ={
    /// 精华
    'good': Option(
      value: TopicType.good,
      label: '精华',
    ),

    /// 分享
    'share': Option(
      value: TopicType.share,
      label: '分享',
    ),
    //...

  };
}

前端生成响应式状态管理

//typescript语言,采用支付宝umi,dva(redux + react-router + redux-saga)
export const topicModel: TopicModel = topicInitModel;
/**  */
topicModel.effects.getTopicPageList = function* ({payload}, {call, put, select}) {
  //为啥不把TopicCommand中的方法直接生成到这里?
  //因为,考虑到方法override时,不过是代码再次组装,而不是再写一遍,这样搞是不是周到、体贴?
  const newPayload = yield TopicCommand.getTopicPageList_effect({payload}, {call, put, select});
  yield put(TopicCommand.getTopicPageList_success_type(newPayload));
};

export class TopicCommand extends BaseCommand {
  /**  */
  static * getTopicPageList_effect({payload}, {call, put, select}) {
    const oldTopicArea = yield select((_) => _.topic.topicArea);
    payload = {page: DEFAULT_PAGE_NUM, pageSize: DEFAULT_PAGE_SIZE, ...payload};
    const topicPageList: AntdPageList<Topic> = yield call(TopicApis.getTopicPageList,
        payload);
    const pagination =topicPageList!.pagination;
    //对上次state,跟据设置前端在状态里自动crud,牛不牛?
    const topics = updateArray(oldTopicArea.list, topicPageList!.list, "topicId");

    const newPayload: TopicState = {
      topicArea: {
        list: topics,
        pagination,
        queryRule: payload,
      },
    };
    return newPayload;
  };
///dat语言,采用目录google官方推荐的provider作为状态管理
abstract class TopicAbstractProvider with ChangeNotifier, BaseProvider, TopicBaseState {
  /// 
  Future<void> getTopicPageList(BuildContext context, {Map<String, dynamic> payload,
    TopicType topicType, bool mdrender, int page, int pageSize }) async {
  //为啥不把TopicCommand中的方法直接生成到这里?
  //因为,考虑到方法override时,不过是代码再次组装,而不是再写一遍,这样搞是不是周到、体贴?
    var newState = await TopicCommand.getTopicPageList(this, payload: payload, topicType:
        topicType, mdrender: mdrender, page: page, pageSize: pageSize);
    mergeState(context, newState);
  }
  //...
}
  
abstract class TopicCommand {

  /// 
  static Future<TopicBaseState> getTopicPageList(TopicAbstractProvider topicState,
    {Map<String, dynamic> payload, TopicType topicType, bool mdrender, int page, int pageSize }) async {
    var oldTopicArea = topicState.topicArea;
    payload ??= {};
    payload = {'pageNum': DEFAULT_PAGE_NUM, 'pageSize': DEFAULT_PAGE_SIZE,  ...payload};
    AntdPageList<Topic> topicPageList = await TopicApis.getTopicPageList(payload: payload,
        topicType: topicType, mdrender: mdrender, page: page, pageSize: pageSize);
    var pagination = topicPageList?.pagination;
    //对上次state,跟据设置前端在状态里自动crud,牛不牛?
    var topicMap = CollectionUtil.appendOrUpdateMap(oldTopicArea?.clone()?.valueMap, 
        Topic.toIdMap(topicPageList.list));

    var newState = _TopicState(
      topicArea: AreaState(
        fetched: true,
        valueMap: topicMap,
        pagination: pagination,
        queryRule: payload,
      ),
    );
    return newState;
  }
  ...
}

前端的同学,不懂后端的话,拼老命写mock(有空开发mock还不如直接告诉后端你想要啥,自己敲后端也行,在stategen中敲后端也是分分钟的事), 你能保证你写的mock与后端一致吗?有了stategen,这些工作是不是完美地解决了?而且,迭代时,直接给你出代码,假如前端排版代码里有兼容问题,编译器和ide都能帮你发现
还有:stategen也可以跟据设置,生成前端对应的状态初始化,刷新、上一页,下一页的前端代码.
还有:我真的非常讨厌字符串硬编码,我是亲眼见过同事为了个大小写问题,节假日加班找bug,有了stategen,不要说节假日省了,都没有996了
还有:...

本说明视频演示请移步Stategen快速调试开发运行精简教程
视频中的相关文档,请见 https://github.com/stategen/docs

打包 前后端 (注意:因为stategen编译前端是在maven test阶段,所以不能添加参数 -Dmaven.test.skip=true)

mvn package 

早期视频

Stategen快速调试开发运行精简教程

详细视频 共6小时

1.stategen之前 微博 趣图
2.stategen简介
3.stategen依赖环境
4.stategen安装和配置
5.生成stategen系统以及工程
6.stategen服务端开发代码演示(上)
7.stategen服务端开发代码演示_迭代开发(中)
8.stategen.mvc
9.stategen前端简介
10.stategen前端dva
11.stategen前端form 生成和实现
12.stategen运行前后端和开发

鸣谢

[react] https://github.com/facebook/react,
[ant-design] https://github.com/ant-design/ant-design
[dva] https://github.com/dvajs/dva
[umi] https://github.com/umijs
[rapid-framework] https://github.com/badqiu/rapid-framework
[zuiidea] https://github.com/zuiidea/antd-admin
[dubbox] https://dangdangdotcom.github.io/dubbox
[spring-framework] https://github.com/spring-projects/spring-framework
...

stategen's Projects

app-frontend-flutter icon app-frontend-flutter

使用Flutter开发的一款商城类的App,并且在持续更新中。项目使用了很多常用三方框架

dalgenx icon dalgenx

dalgenX是对支付宝dalgen代码生成器的开源实现,freemarker版,可迭代开发生成器,同时支持ibatis和mybatis

dubbo icon dubbo

Apache Dubbo is a high-performance, java based, open source RPC framework.

issrc icon issrc

Inno Setup is a free installer for Windows programs. First introduced in 1997, Inno Setup today rivals and even surpasses many commercial installers in feature set and stability.

pay-java-parent icon pay-java-parent

第三方支付对接全能支付Java开发工具包.优雅的轻量级支付模块集成支付对接支付整合(微信,支付宝,银联,友店,富友,跨境支付paypal,payoneer(P卡派安盈)易极付)app,扫码,网页刷脸付刷卡付条码付转账服务商模式、支持多种支付类型多支付账户,支付与业务完全剥离,简单几行代码即可实现支付,简单快速完成支付模块的开发,可轻松嵌入到任何系统里 目前仅是一个开发工具包(即SDK),只提供简单Web实现,建议使用maven或gradle引用本项目即可使用本SDK提供的各种支付相关的功能

stategen icon stategen

springboot+spring cloud alibaba(nacos,seata,sentinel,dubbo)分布式+微服务+本地服务,全栈架构含有前端(java服务端,H5、iOS、andriod前端),后端骨架代码生成器+前端骨架生成器+后端迭代开发生成器+前端与后端交互(至响应式状态)生成器,内含大名鼎鼎的支付宝dalgen之freemarker开源实现之商用升级版dalgenX,大量前、后端代码通过生成器生成,把前后端分离开发模式"拉"回来,目前前端已支持react(dva+umi+typescript)和flutter(provider),后续加入kotlin、swift。免去前端文档、调试、postman、mockjs...繁琐。开发中迭代生成,不改变原开发流程、生成80%代码,兼容后20%你自己的代码,拒绝挖坑! https://github.com/stategen/stategen

virtualtreeview icon virtualtreeview

Virtual Treeview is a Delphi treeview control built from ground up. Many years of development made it one of the most flexible and advanced tree controls available today. Virtual Treeview starts off with the claim to improve many aspects of existing solutions and introduces some new technologies and principles which were not available before.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.