首页 文章 精选 留言 我的

精选列表

搜索[crud],共2299篇文章
优秀的个人博客,低调大师

go-zero微服务实战系列(四、CRUD热热身)

上一篇文章我们把整个项目的架子搭建完成,服务在本地也已经能运行起来了,顺利成章的接下来我们就应该开始写业务逻辑代码了,但是单纯的写业务逻辑代码是比较枯燥的,业务逻辑的代码我会不断地补充到 lerbon 项目中去,关键部分我也会加上注释。 那么本篇文章我主要想和大家分享下服务的基本配置和几个典型的代码示例。 日志定义 go-zero的 logx 包提供了日志功能,默认不需要做任何配置就可以在stdout中输出日志。当我们请求/v1/order/list接口的时候输出日志如下,默认是json格式输出,包括时间戳,http请求的基本信息,接口耗时,以及链路追踪的span和trace信息。 {"@timestamp":"2022-06-11T08:23:36.342+08:00","caller":"handler/loghandler.go:197","content":"[HTTP] 200 - GET /v1/order/list?uid=123 - 127.0.0.1:59998 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36","duration":"21.2ms","level":"info","span":"23c4deaa3432fd03","trace":"091ffcb0eafe7818b294e4d8122cf8a1"} 程序启动后,框架会默认输出level为stat的统计日志,用于输出当前资源的使用情况,主要为cpu和内存,内容如下: {"@timestamp":"2022-06-11T08:34:58.402+08:00","caller":"stat/usage.go:61","content":"CPU: 0m, MEMORY: Alloc=3.3Mi, TotalAlloc=7.0Mi, Sys=16.3Mi, NumGC=8","level":"stat"} 当我们不需要这类日志的时候,我们可以通过如下方式关闭该类日志的输出: logx.DisableStat() 有的时候我们只需要记录错误日志,可以通过设置日志等级来取消level为info级别日志的输出: logx.SetLevel(logx.ErrorLevel) 可以扩展日志输出的字段,添加了uid字段记录请求的用户的uid,日志打印内容如下: logx.Infow("order list", logx.Field("uid",req.UID)) {"@timestamp":"2022-06-11T08:53:50.609+08:00","caller":"logic/orderlistlogic.go:31","content":"order list","level":"info","uid":123} 我们还可以扩展其他第三方日志库,通过logx.SetWriter来进行设置 writer := logrusx.NewLogrusWriter(func(logger *logrus.Logger) { logger.SetFormatter(&logrus.JSONFormatter{}) }) logx.SetWriter(writer) 同时logx还提供了丰富的配置,可以配置日志输出模式,时间格式,输出路径,是否压缩,日志保存时间等 type LogConf struct { ServiceName string `json:",optional"` Mode string `json:",default=console,options=[console,file,volume]"` Encoding string `json:",default=json,options=[json,plain]"` TimeFormat string `json:",optional"` Path string `json:",default=logs"` Level string `json:",default=info,options=[info,error,severe]"` Compress bool `json:",optional"` KeepDays int `json:",optional"` StackCooldownMillis int `json:",default=100"` } 可以看到logx提供的日志功能还是非常丰富的,同时支持了各种自定义的方式。日志是我们排查线上问题非常重要的依赖,我们还会根据日志做各种告警,所以这里我们先做了一些日志使用的介绍。 服务依赖 在BFF服务中会依赖多个RPC服务,默认情况下,如果依赖的RPC服务没有启动,BFF服务也会启动异常,报错如下,通过日志可以知道是因为order.rpc没有启动,因为order.rpc是整个商城系统的核心服务,BFF对order.rpc是强依赖,在强依赖的情况下如果被依赖服务异常,那么依赖服务也无法正常启动。 {"@timestamp":"2022-06-11T10:21:56.711+08:00","caller":"internal/discovbuilder.go:34","content":"bad resolver state","level":"error"} 2022/06/11 10:21:59 rpc dial: discov://127.0.0.1:2379/order.rpc, error: context deadline exceeded, make sure rpc service "order.rpc" is already started exit status 1 再看如下的场景,BFF依赖reply.rpc,因为reply.rpc异常导致BFF无法正常启动,由于reply.rpc并不是商城系统的核心依赖,就算reply.rpc挂掉也不影响商城的核心流程,所以对于BFF来说reply.rpc是弱依赖,在弱依赖的情况下不应该影响依赖方的启动。 {"@timestamp":"2022-06-11T11:26:51.711+08:00","caller":"internal/discovbuilder.go:34","content":"bad resolver state","level":"error"} 2022/06/11 11:26:54 rpc dial: discov://127.0.0.1:2379/reply.rpc, error: context deadline exceeded, make sure rpc service "reply.rpc" is already started exit status 1 在go-zero中提供了弱依赖的配置,配置后BFF即可正常启动,可以看到order.rpc和product.rpc都是强依赖,而reply.rpc配置了NonBlock:true为弱依赖 OrderRPC: Etcd: Hosts: - 127.0.0.1:2379 Key: order.rpc ProductRPC: Etcd: Hosts: - 127.0.0.1:2379 Key: product.rpc ReplyRPC: Etcd: Hosts: - 127.0.0.1:2379 Key: reply.rpc NonBlock: true 并行调用 在高并发的系统中,接口耗时是我们非常关注的点,接口快速响应可以提升用户体验,长时间的等待会让用户体验很差,用户也就会慢慢的离开我们。这里我们介绍简单但很实用的提升接口响应时间的方法,那就是并行的依赖调用。 下图展示了串行调用和并行调用的区别,串行调用依赖的话,耗时等于所有依赖耗时的和,并行调用依赖的话,耗时等于所有依赖中耗时最大的一个依赖的耗时。 在获取商品详情的接口中,参数ProductIds为逗号分隔的多个商品id,在这里我们使用go-zero提供的mapreduce来并行的根据商品id获取商品详情,代码如下,详细代码请参考product-rpc服务: func (l *ProductsLogic) Products(in *product.ProductRequest) (*product.ProductResponse, error) { products := make(map[int64]*product.ProductItem) pdis := strings.Split(in.ProductIds, ",") ps, err := mr.MapReduce(func(source chan<- interface{}) { for _, pid := range pdis { source <- pid } }, func(item interface{}, writer mr.Writer, cancel func(error)) { pid := item.(int64) p, err := l.svcCtx.ProductModel.FindOne(l.ctx, pid) if err != nil { cancel(err) return } writer.Write(p) }, func(pipe <-chan interface{}, writer mr.Writer, cancel func(error)) { var r []*model.Product for p := range pipe { r = append(r, p.(*model.Product)) } writer.Write(r) }) if err != nil { return nil, err } for _, p := range ps.([]*model.Product) { products[p.Id] = &product.ProductItem{ ProductId: p.Id, Name: p.Name, } } return &product.ProductResponse{Products: products}, nil } 在商品详情页,不仅展示了商品的详情,同时页展示了商品评价的第一页,然后点击评价详情可以跳转到评价详情页,为了避免客户端同时请求多个接口,所以我们在商品详情页把评论首页的内容一并返回,因为评论内容并不是核心内容所以在这里我们还做了降级,即请求reply.rpc接口报错我们会忽略这个错误,从而能让商品详情正常的展示。因为获取商品详情和商品评价没有前后依赖关系,所以这里我们使用mr.Finish来并行的请求来降低接口的耗时。 func (l *ProductDetailLogic) ProductDetail(req *types.ProductDetailRequest) (resp *types.ProductDetailResponse, err error) { var ( p *product.ProductItem cs *reply.CommentsResponse ) if err := mr.Finish(func() error { var err error if p, err = l.svcCtx.ProductRPC.Product(l.ctx, &product.ProductItemRequest{ProductId: req.ProductID}); err != nil { return err } return nil }, func() error { var err error if cs, err = l.svcCtx.ReplyRPC.Comments(l.ctx, &reply.CommentsRequest{TargetId: req.ProductID}); err != nil { logx.Errorf("get comments error: %v", err) } return nil }); err != nil { return nil, err } var comments []*types.Comment for _, c := range cs.Comments { comments = append(comments, &types.Comment{ ID: c.Id, Content: c.Content, }) } return &types.ProductDetailResponse{ Product: &types.Product{ ID: p.ProductId, Name: p.Name, }, Comments: comments, }, nil } 图片上传 图片上传是非常常用的功能,我们在product-admin中需要上传商品图片,这里我们把商品图片上传到阿里云OSS中,api定义如下 syntax = "v1" type UploadImageResponse { Success bool `json:"success"` } service admin-api { @handler UploadImageHandler post /v1/upload/image() returns (UploadImageResponse) } 在admin-api.yaml中添加如下配置 Name: admin-api Host: 0.0.0.0 Port: 8888 OSSEndpoint: https://oss-cn-hangzhou.aliyuncs.com AccessKeyID: xxxxxxxxxxxxxxxxxxxxxxxx AccessKeySecret: xxxxxxxxxxxxxxxxxxxxxxxx 添加OSS客户端 type ServiceContext struct { Config config.Config OssClient *oss.Client } func NewServiceContext(c config.Config) *ServiceContext { oc, err := oss.New(c.OSSEndpoint, c.AccessKeyID, c.AccessKeySecret) if err != nil { panic(err) } return &ServiceContext{ Config: c, OssClient: oc, } } 上传逻辑需要先获取bucket,该bucket为预先定义的bucket,可以通过api调用创建,也可以在阿里云工作台手动创建 func (l *UploadImageLogic) UploadImage() (resp *types.UploadImageResponse, err error) { file, header, err := l.r.FormFile(imageFileName) if err != nil { return nil, err } defer file.Close() bucket, err := l.svcCtx.OssClient.Bucket(bucketName) if err != nil { return nil, err } if err = bucket.PutObject(header.Filename, file); err != nil { return nil, err } return &types.UploadImageResponse{Success: true}, nil } 使用Postman上传图片,注意在上传图片前需要先创建bucket 登录阿里云对象存储查看已上传的图片 结束语 本篇文章通过日志定义和服务依赖介绍了服务构建中常见的一些配置,这里并没有把所有配置一一列举而是举例说明了社区中经常有人问到的场景,后面的文章还会继续不断完善服务的相关配置。接着又通过服务依赖的并行调用和图片上传两个案例展示了常见功能的优化手段以及编码方式。 这里并没有把所有的功能都列出来,也是想起个头,大家可以把项目down下来自己去完善这个项目,纸上得来终觉浅,绝知此事要躬行,当然我也会继续完善项目代码和大家一起学习进步。 希望本篇文章对你有所帮助,谢谢。 每周一、周四更新 代码仓库 项目地址 https://github.com/zeromicro/go-zero https://gitee.com/kevwan/go-zero 欢迎使用 go-zero 并 star 支持我们! 微信交流群 关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

优秀的个人博客,低调大师

每日一博 | 突破 CRUD | 万能树工具类封装

0、学完本文你或许可以收获 感受一个树工具从初始逐步优化完善的过程 树工具封装的设计思考与实现思路 最后收获一款拿来即用的树工具源代码 对于前端树组件有一定了解和使用过的同学可直接跳跃到第3章节开始。 1、树长什么样 ? 前端的树组件大多数情况下出现在后端的管理系统中,比如我们常见的菜单树、机构树、某某分类树、树表格等。大致像下方图片所展示的这样。 菜单树 机构树 org_tree.png 树表格 大致上来说,前端树的展现形式就是上面3张图所列的几种形式。而这种前端树组件的展现构成需要依赖于后端返回的数据格式。 2、数据格式 结合我自身使用过的前端树组件来说,大致可以分为如下两种。 列表形式 [ {id:1,pId:0,name:"父节点1"} {id:11,pId:1,name:"父节点11"}, {id:111,pId:11,name:"叶子节点111"}, {id:112,pId:11,name:"叶子节点112"}, {id:113,pId:11,name:"叶子节点113"}, {id:114,pId:11,name:"叶子节点114"}, {id:12,pId:1,name:"父节点12"}, {id:121,pId:12,name:"叶子节点121"}, {id:122,pId:12,name:"叶子节点122"}, {id:123,pId:12,name:"叶子节点123"}, {id:124,pId:12,name:"叶子节点124"} ] 树形结构 [{name:"父节点1", children:[ { name:"父节点11", children:[ {name:"叶子节点111"}, {name:"叶子节点112"}, {name:"叶子节点113"}, {name:"叶子节点114"} ] }, { name:"父节点12", children:[ {name:"叶子节点121"}, {name:"叶子节点122"}, {name:"叶子节点123"}, {name:"叶子节点124"} ] } ] }] 本文所讲的树工具封装主要是针对第二种数据格式树形结构来说,因为第一种本身不需要特殊处理,也就不存在什么封装,就是简单的列表查询展示,与一般数据列表数据格式的区别是多了数据ID与父ID属性提供给前端进行树组件的构造。 而第二种是要在列表形式的数据格式上进行转换,形成如上所示的树形结构。但是,我们发现里面没有数据ID与父ID属性,why ? 因为后端完成了数据层面树结构的构造工作,前端树组件再无需根据这两个属性进行树结构的判断构建,直接展示就OK,当然也不绝对,最终还得看前端的树组件是否需要。 但一般都会保留这两个属性,因为除过树组件自身的构造需求,业务处理上往往需要这两个属性,而后端树工具要构造树结构,那一定是需要数据ID与父ID的。 如果感觉上面说的麻烦你就记住一点,不管是列表结构还是树形结构,始终保留数据ID与父ID两个属性就对了。 到这里又有一个新问题了,上面说了列表形式无需封装什么可以直接使用,既然如此那用列表形式的结构就完了呗,为什么写个工具类搞个树结构出来呢 ? 原因是,前端树组件的实现方式非常多,不同树插件或组件需要的数据格式可能不一样,有的列表、树形格式都支持,有的仅支持列表或树形的一种,所以为了满足不同前端树的展示需求,提供树形结构的构造工具是必要的。 3、话不多说,先实现个初版 从上面的内容我们了解了前端树组件的渲染展现需要后端提供满足需求的数据格式,那么实际上也就决定了树工具类的核心职责就是将一般的数据列表结构转换为树形结构,从而提供给前端使用。 解读上面所述的核心职责,首先一般列表是什么列表,此处我们假设为菜单列表,这就有了第一个类MenuEntity,紧接着是转换,谁转换成谁 ?数据列表转换树结构,树结构本身那应该就是个类,我们暂且叫它 TreeNode,结合我们第一步假设的菜单列表,那实际上就是 List< MenuEntity > 转换为 List < TreeNode > ,如此就得到了第二个类TreeNode,最后还剩转换这个动作谁去做 ? 那就是我们今天的主角 TreeUtil 了。 好,至此,通过分析树工具类的核心职责,我们分析得到了三个类。 MenuEntity TreeNode TreeUtil OK,有了上面的内容那就来个简单的实现。 树节点类 publicclassTreeNode{ //树节点ID privateStringid; //树节点名称 privateStringname; //树节点编码 privateStringcode; //树节点链接 privateStringlinkUrl; //树节点图标 privateStringicon; //父节点ID privateStringparentId; } 菜单类 publicclassMenuEntity{ //菜单ID privateStringid; //上级菜单ID privateStringpid; //菜单名称 privateStringname; //菜单编码 privateStringcode; //菜单图标 privateStringicon; //菜单链接 privateStringurl; } 树工具类 publicclassTreeUtil{ /** *树构建 */ publicstaticList<TreeNode>build(List<TreeNode>treeNodes,ObjectparentId){ List<TreeNode>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodetreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } privatestaticvoidinnerBuild(List<TreeNode>treeNodes,TreeNodeparentNode){ for(TreeNodechildNode:treeNodes){ if(parentNode.getId().equals(childNode.getParentId())){ List<TreeNode>children=parentNode.getChildren(); if(children==null){ children=CollectionUtil.newArrayList(); parentNode.setChildren(children); } children.add(childNode); childNode.setParentId(parentNode.getId()); innerBuild(treeNodes,childNode); } } } } 树工具类实现的两个关键点,第一,树构建的开始位置也就是从哪里开始构建,所以需要一个父ID参数来指定构建的起始位置,第二,构建到什么时候结束,不做限制的的话,我们的树是可以无限延伸的,所以此处innerBuild方法进行递归操作。 测试代码 publicstaticvoidmain(String[]args){ //1、模拟菜单数据 List<MenuEntity>menuEntityList=CollectionUtil.newArrayList(); menuEntityList.add(newMenuEntity("1","0","系统管理","sys","/sys")); menuEntityList.add(newMenuEntity("11","1","用户管理","user","/sys/user")); menuEntityList.add(newMenuEntity("111","11","用户添加","userAdd","/sys/user/add")); menuEntityList.add(newMenuEntity("2","0","店铺管理","store","/store")); menuEntityList.add(newMenuEntity("21","2","商品管理","shop","/shop")); //2、MenuEntity->TreeNode List<TreeNode>treeNodes=CollectionUtil.newArrayList(); for(MenuEntitymenuEntity:menuEntityList){ TreeNodetreeNode=newTreeNode(); treeNode.setId(menuEntity.getId()); treeNode.setParentId(menuEntity.getPid()); treeNode.setCode(menuEntity.getCode()); treeNode.setName(menuEntity.getName()); treeNode.setLinkUrl(menuEntity.getUrl()); treeNodes.add(treeNode); } //3、树结构构建 List<TreeNode>treeStructureNodes=TreeUtil.build(treeNodes,"0"); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes))); } 收工,第一版简单的树工具就实现了。 4、迭代优化 1.0 这不是我的事 但是,通过测试代码我们发现这个用起来不是太爽,要将菜单数据转换为树结构竟然需要我先把菜单列表转换成树结构的列表才能调用树工具类的build方法,这里的转换操作仅仅是属性的拷贝,并未完成树状结构的生成构建,但这是调用者需要关心的吗 ?很显然TreeNode集合创建生成这个过程应该是树工具类应该做的事情。所以做了如下调整。 1 调整了build方法参数,将原有treeNodes 调整为 menuEntityList,意味着将上面说的treeNodes构建构成交给TreeUtil去做。 2 新增了Convert类,并包含convert方法,该方法的职责是完成菜单实体到树节点属性的拷贝。 3 再次调整build方法参数,新增Convert转换。 调整完成的结果,看下代码。 树工具 publicclassTreeUtil_1_0{ //新增的属性转换方法 publicinterfaceConvert<MenuEntity,TreeNode>{ publicvoidconvert(MenuEntitymenuEntity,TreeNodetreeNode); } /** *树构建 */ publicstaticList<TreeNode>build(List<MenuEntity>menuEntityList,ObjectparentId,Convert<MenuEntity,TreeNode>convert){ //原来调用方做的事情 List<TreeNode>treeNodes=CollectionUtil.newArrayList(); for(MenuEntitymenuEntity:menuEntityList){ TreeNodetreeNode=newTreeNode(); convert.convert(menuEntity,treeNode); treeNodes.add(treeNode); } List<TreeNode>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodetreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } privatestaticvoidinnerBuild(List<TreeNode>treeNodes,TreeNodeparentNode){ for(TreeNodechildNode:treeNodes){ if(parentNode.getId().equals(childNode.getParentId())){ List<TreeNode>children=parentNode.getChildren(); if(children==null){ children=CollectionUtil.newArrayList(); parentNode.setChildren(children); } children.add(childNode); childNode.setParentId(parentNode.getId()); innerBuild(treeNodes,childNode); } } } } 测试代码 publicstaticvoidmain(String[]args){ //1、模拟菜单数据 List<MenuEntity>menuEntityList=CollectionUtil.newArrayList(); menuEntityList.add(newMenuEntity("1","0","系统管理","sys","/sys")); menuEntityList.add(newMenuEntity("11","1","用户管理","user","/sys/user")); menuEntityList.add(newMenuEntity("111","11","用户添加","userAdd","/sys/user/add")); menuEntityList.add(newMenuEntity("2","0","店铺管理","store","/store")); menuEntityList.add(newMenuEntity("21","2","商品管理","shop","/shop")); //2、树结构构建 List<TreeNode>treeStructureNodes=TreeUtil_1_0.build(menuEntityList,"0",newConvert<MenuEntity,TreeNode>(){ @Override publicvoidconvert(MenuEntitymenuEntity,TreeNodetreeNode){ treeNode.setId(menuEntity.getId()); treeNode.setParentId(menuEntity.getPid()); treeNode.setCode(menuEntity.getCode()); treeNode.setName(menuEntity.getName()); treeNode.setLinkUrl(menuEntity.getUrl()); } }); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes))); } 比较1.0与初版的测试代码,发现少了树节点列表构建的过程,属性拷贝的工作作为回调过程在转换过程中进行处理。 2.0 仅支持造菜单树哪够 1.0优化完后,我们来了新的需求,有个机构树也需要生成,此时的树工具仅支持了菜单树,所以我们进行改造,让其支持其他任何对象的树生成。 改造点主要是将的TreeUtil中的菜单实体转换为泛型,限于篇幅,就贴个核心方法的代码 publicstatic<T>List<TreeNode>build(List<T>list,ObjectparentId,Convert<T,TreeNode>convert){ List<TreeNode>treeNodes=CollectionUtil.newArrayList(); for(Tobj:list){ TreeNodetreeNode=newTreeNode(); convert.convert(obj,treeNode); treeNodes.add(treeNode); } List<TreeNode>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodetreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } 如此一来,我们就可以支持任意类型的树构造。 3.0 哥们,你返回的属性不够用啊 前两点比较容易想到,也比较容易实现,但这时候前端同学抛来了新的问题,哥们,你返回的树节点属性不够用啊,你看我这界面。需要备注你没返回来啊。 好吧,这种情况确实没考虑到。 要满足上述需求,简单做法就将remark属性直接添加到 TreeNode 类中,Convert中赋下值,这不就满足了,但想想又不对,今天这个前端伙计缺个remark,明天可能别的伙计又缺个其他属性,全加到TreeNode中,TreeNode到底是树节点还是业务实体,所以不能这么搞。 这里要处理成可扩展,同时满足开闭原则,所以此处比较妥的处理方式是继承,TreeNode属性满足不了的情况下,通过继承扩展具体业务的树节点来实现。 具体改造点如下 1 新增菜单实体扩展树节点如下 publicclassMenuEntityTreeNodeextendsTreeNode{ //扩展备注属性 privateStringremark; //省略setget... } 2 改造TreeUtil.build方法参数,新增TreeNode Class类型参数,如下 /** *树构建 */ publicstatic<T,EextendsTreeNode>List<E>build(List<T>list,ObjectparentId,Class<E>treeNodeClass,Convert<T,E>convert){ List<E>treeNodes=CollectionUtil.newArrayList(); for(Tobj:list){ EtreeNode=(E)ReflectUtil.newInstance(treeNodeClass); convert.convert(obj,treeNode); treeNodes.add(treeNode); } List<E>finalTreeNodes=CollectionUtil.newArrayList(); for(EtreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add((E)treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } 测试代码 publicstaticvoidmain(String[]args){ //...此处省略模拟数据创建过程 //2、树结构构建 List<MenuEntityTreeNode>treeStructureNodes=TreeUtil_3_0.build(menuEntityList,"0",MenuEntityTreeNode.class,newTreeUtil_3_0.Convert<MenuEntity,MenuEntityTreeNode>(){ @Override publicvoidconvert(MenuEntityobject,MenuEntityTreeNodetreeNode){ treeNode.setId(object.getId()); treeNode.setParentId(object.getPid()); treeNode.setCode(object.getCode()); treeNode.setName(object.getName()); treeNode.setLinkUrl(object.getUrl()); //新增的业务属性 treeNode.setRemark("添加备注属性"); } }); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes))); } 如此一来,不同业务场景下需要添加不同的属性时,即可做到可扩展,且对现有代码不造成任何影响和改动。 4.0 哥们,我的属性名不叫code 完成了3.0版本,基本上大部分需求就都可以满足了,但是这时候前端同学又抛来了新的问题,哥们,你返回的树节点编号属性是code,但我这边的叫number,对应不上,我这边调整的话影响比较大,你看后端返回的时候能不能处理下。 code属性名肯定是不能调整的,因为其他模块树的节点编号都叫code。 那怎么办 ?其实也简单,跟3.0版本一样,在扩展的业务树节点去加个属性,这样问题是解决了,但万一出现所有treeNode的属性名都跟前端需要的不对应这种极端情况,那意味着所有树属性都需要自行扩展定义,这种岂不是返回了没什么用的父TreeNode心中的所有属性。序列化时倒是可以控制,为空的不进行序列化,但不是依赖序列化框架了么。还有没有其他办法。 稍微整理下需求,就是树节点属性在返回前端时要能够支持自定义属性名。 类属性定义好就改不了了,怎么自定义,除了新增类和改现有的属性,还有什么办法呢 ?这时候我们应该想到map 具体怎么做 1 首先,定义新的类TreeNodeMap,看名字就知道基于map实现 publicclassTreeNodeMapextendsHashMap{ privateTreeNodeConfigtreeNodeConfig; publicTreeNodeMap(){ this.treeNodeConfig=TreeNodeConfig.getDefaultConfig(); } publicTreeNodeMap(TreeNodeConfigtreeNodeConfig){ this.treeNodeConfig=treeNodeConfig; } public<T>TgetId(){ return(T)super.get(treeNodeConfig.getIdKey()); } publicvoidsetId(Stringid){ super.put(treeNodeConfig.getIdKey(),id); } public<T>TgetParentId(){ return(T)super.get(treeNodeConfig.getParentIdKey()); } publicvoidsetParentId(StringparentId){ super.put(treeNodeConfig.getParentIdKey(),parentId); } public<T>TgetName(){ return(T)super.get(treeNodeConfig.getNameKey()); } publicvoidsetName(Stringname){ super.put(treeNodeConfig.getNameKey(),name); } public<T>TgetCode(){ return(T)super.get(treeNodeConfig.getCodeKey()); } publicTreeNodeMapsetCode(Stringcode){ super.put(treeNodeConfig.getCodeKey(),code); returnthis; } publicList<TreeNodeMap>getChildren(){ return(List<TreeNodeMap>)super.get(treeNodeConfig.getChildrenKey()); } publicvoidsetChildren(List<TreeNodeMap>children){ super.put(treeNodeConfig.getChildrenKey(),children); } publicvoidextra(Stringkey,Objectvalue){ super.put(key,value); } } 2 既然支持属性名自定义,新增配置类TreeNodeConfig来完成这个事情,同时提供默认属性名 publicclassTreeNodeConfig{ //默认属性的单例对象 privatestaticTreeNodeConfigdefaultConfig=newTreeNodeConfig(); //树节点默认属性常量 staticfinalStringTREE_ID="id"; staticfinalStringTREE_NAME="name"; staticfinalStringTREE_CODE="code"; staticfinalStringTREE_CHILDREN="children"; staticfinalStringTREE_PARENT_ID="parentId"; //属性 privateStringidKey; privateStringcodeKey; privateStringnameKey; privateStringchildrenKey; privateStringparentIdKey; publicStringgetIdKey(){ returngetOrDefault(idKey,TREE_ID); } publicvoidsetIdKey(StringidKey){ this.idKey=idKey; } publicStringgetCodeKey(){ returngetOrDefault(codeKey,TREE_CODE); } publicvoidsetCodeKey(StringcodeKey){ this.codeKey=codeKey; } publicStringgetNameKey(){ returngetOrDefault(nameKey,TREE_NAME); } publicvoidsetNameKey(StringnameKey){ this.nameKey=nameKey; } publicStringgetChildrenKey(){ returngetOrDefault(childrenKey,TREE_CHILDREN); } publicvoidsetChildrenKey(StringchildrenKey){ this.childrenKey=childrenKey; } publicStringgetParentIdKey(){ returngetOrDefault(parentIdKey,TREE_PARENT_ID); } publicvoidsetParentIdKey(StringparentIdKey){ this.parentIdKey=parentIdKey; } publicStringgetOrDefault(Stringkey,StringdefaultKey){ if(key==null){ returndefaultKey; } returnkey; } publicstaticTreeNodeConfiggetDefaultConfig(){ returndefaultConfig; } } 3 最后,改造TreeUtil.build 方法,基于2.0版本,只需将TreeNode替换成TreeNodeMap即可。 /** *树构建 */ publicstatic<T>List<TreeNodeMap>build(List<T>list,ObjectparentId,Convert<T,TreeNodeMap>convert){ List<TreeNodeMap>treeNodes=CollectionUtil.newArrayList(); for(Tobj:list){ TreeNodeMaptreeNode=newTreeNodeMap(); convert.convert(obj,treeNode); treeNodes.add(treeNode); } List<TreeNodeMap>finalTreeNodes=CollectionUtil.newArrayList(); for(TreeNodeMaptreeNode:treeNodes){ if(parentId.equals(treeNode.getParentId())){ finalTreeNodes.add(treeNode); innerBuild(treeNodes,treeNode); } } returnfinalTreeNodes; } 测试代码 publicstaticvoidmain(String[]args){ //...省略菜单模拟数据的创建过程 TreeNodeConfigtreeNodeConfig=newTreeNodeConfig(); //自定义属性名 treeNodeConfig.setCodeKey("number"); List<TreeNodeMap>treeNodes=TreeUtil_4_0.build(menuEntityList,"0",treeNodeConfig,newTreeUtil_4_0.Convert<MenuEntity,TreeNodeMap>(){ @Override publicvoidconvert(MenuEntityobject,TreeNodeMaptreeNode){ treeNode.setId(object.getId()); treeNode.setParentId(object.getPid()); treeNode.setCode(object.getCode()); treeNode.setName(object.getName()); //属性扩展 treeNode.extra("extra1","123"); } }); Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeNodes))); } 经过上面的改造,我们实现了树节点属性的自定义,顺便还实现了属性可扩展,一举两得。 3、总结 目前这个程度可能仍有些场景无法满足,但是对于大部分的问题场景基于3.0或4.0版本稍加改造应该都可以解决。剩下的就结合场景再酌情优化调整。 4、源代码&视频 springboot-tree 5、更多精彩 觉得还行,动动手指留个赞。 以上就是今天的内容,我们下期见。 更多优质内容,首发公众号【风象南】,欢迎关注。

优秀的个人博客,低调大师

Mybatisplus-plus 1.3.1 发布,新增服务层根据复合主键 CRUD 操作

Mybatisplus-plus 1.3.1新增在service层操作复合主键进行增删改查相关操作的功能。 **从中央库引入jar** ```` <dependency> <groupId>com.github.jeffreyning</groupId> <artifactId>mybatisplus-plus</artifactId> <version>1.3.1-RELEASE</version> </dependency> ```` 在实例类成员变量上使用@MppMultiId表明联合主键 ```` @TableName("test07") public class Test07Entity { @MppMultiId @TableField(value = "k1") private Integer k1; @MppMultiId @TableField(value = "k2") private String k2; @TableField(value = "col1") private String col1; @TableField(value = "col2") private String col2; ```` mapper需要继承MppBaseMapper ```` @Mapper public interface Test07Mapper extends MppBaseMapper<Test07Entity> { } ```` ```` service层继承IMppService ```` public interface Test07Service extends IMppService<Test07Entity> { } @Service public class Test07ServiceImpl extends ServiceImpl<Test07Mapper, Test07Entity> implements Test07Service { } ```` 在service层调用多主键操作 ```` public void testMultiIdService(){ //id Test07Entity idEntity=new Test07Entity(); idEntity.setK1(1); idEntity.setK2("111"); //del test07Service.deleteByMultiId(idEntity); //add test07Service.save(idEntity); //query Test07Entity retEntity=test07Service.selectByMultiId(idEntity); retEntity.setCol1("xxxx"); //update test07Mapper.updateByMultiId(retEntity); } ````

优秀的个人博客,低调大师

用SAP WebIDE将CRUD Master-Detail应用打包成Hybrid App

登录api.sap.com, 找到自己想用ABAP调用的API: API References里,点击Code Snippt, 即可看到API Hub自动生成的使用ABAP调用该API的代码: 在SAP WebIDE里,选择Tools->Preferences, 启用Hybrid App Toolkit: 选择WebIDE里的Master-Detail应用文件夹,右键菜单里选择Mobile->Enable as Hybrid Mobile Project: 这个选择会添加一个名叫mobile的文件夹到当前项目工程里,包含了一些该应用在设备或者模拟器里运行所必须的一些文件: Build Packaged App: 选择应用运行的移动平台类型: Build成功: 本文来自云栖社区合作伙伴“汪子熙”,了解相关信息可以关注微信公众号"汪子熙"。

优秀的个人博客,低调大师

mybatis-plus-generator 3.5.1 发布,一行代码写完 crud 代码生成

👉 源码(opens new window) #安装 👉 点击查看最新版本(opens new window) <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>Latest Version</version> </dependency> #使用(以下教程仅适用 3.5.1 以上版本,对历史版本的不兼容) #快速生成 FastAutoGenerator.create("url", "username", "password") .globalConfig(builder -> { builder.author("baomidou") // 设置作者 .enableSwagger() // 开启 swagger 模式 .fileOverride() // 覆盖已生成文件 .outputDir("D://"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名 .moduleName("system") // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude("t_simple") // 设置需要生成的表名 .addTablePrefix("t_", "c_"); // 设置过滤表前缀 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute(); #交互式生成 FastAutoGenerator.create(DATA_SOURCE_CONFIG) // 全局配置 .globalConfig((scanner, builder) -> builder.author(scanner.apply("请输入作者名称?")).fileOverride()) // 包配置 .packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?"))) // 策略配置 .strategyConfig(builder -> builder.addInclude("t_simple")) /* 模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker .templateEngine(new BeetlTemplateEngine()) .templateEngine(new FreemarkerTemplateEngine()) */ .execute(); 更多例子可查看test包下面的samples H2CodeGeneratorTest(opens new window) FastAutoGeneratorTest(opens new window) #说明 #数据库配置(DataSourceConfig) #基础配置 属性 说明 示例 url jdbc路径 jdbc:mysql://127.0.0.1:3306/mybatis-plus username 数据库账号 root password 数据库密码 123456 new DataSourceConfig. Builder("jdbc:mysql://127.0.0.1:3306/mybatis-plus","root","123456").build(); #可选配置 方法 说明 示例 dbQuery(IDbQuery) 数据库查询 new MySqlQuery() schema(String) 数据库schema(部分数据库适用) mybatis-plus typeConvert(ITypeConvert) 数据库类型转换器 new MySqlTypeConvert() keyWordsHandler(IKeyWordsHandler) 数据库关键字处理器 new MySqlKeyWordsHandler() new DataSourceConfig.Builder("jdbc:mysql://127.0.0.1:3306/mybatis-plus","root","123456") .dbQuery(new MySqlQuery()) .schema("mybatis-plus") .typeConvert(new MySqlTypeConvert()) .keyWordsHandler(new MySqlKeyWordsHandler()) .build(); #全局配置(GlobalConfig) 方法 说明 示例 fileOverride 覆盖已生成文件 默认值:false disableOpenDir 禁止打开输出目录 默认值:true outputDir(String) 指定输出目录 /opt/baomidou/ 默认值: windows:D:// linux or mac : /tmp author(String) 作者名 baomidou 默认值:作者 enableKotlin 开启 kotlin 模式 默认值:false enableSwagger 开启 swagger 模式 默认值:false dateType(DateType) 时间策略 DateType.ONLY_DATE 默认值: DateType.TIME_PACK commentDate(String) 注释日期 默认值: yyyy-MM-dd new GlobalConfig.Builder(). .fileOverride() .outputDir("/opt/baomidou") .author("baomidou") .enableKotlin() .enableSwagger() .dateType(DateType.TIME_PACK) .commentDate("yyyy-MM-dd") .build(); #包配置(PackageConfig) 方法 说明 示例 parent(String) 父包名 默认值:com.baomidou moduleName(String) 父包模块名 默认值:无 entity(String) Entity 包名 默认值:entity service(String) Service 包名 默认值:service serviceImpl(String) Service Impl 包名 默认值:service.impl mapper(String) Mapper 包名 默认值:mapper mapperXml(String) Mapper XML 包名 默认值:mapper.xml controller(String) Controller 包名 默认值:controller other(String) 自定义文件包名 输出自定义文件时所用到的包名 pathInfo(Map<OutputFile, String>) 路径配置信息 Collections.singletonMap(OutputFile.mapperXml, "D://") new PackageConfig.Builder() .parent("com.baomidou.mybatisplus.samples.generator") .moduleName("sys") .entity("po") .service("service") .serviceImpl("service.impl") .mapper("mapper") .mapperXml("mapper.xml") .controller("controller") .other("other") .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://") .build(); #模板配置(TemplateConfig) 方法 说明 示例 disable 禁用所有模板 disable(TemplateType...) 禁用模板 TemplateType.ENTITY entity(String) 设置实体模板路径(JAVA) /templates/entity.java entityKt(String) 设置实体模板路径(kotlin) /templates/entity.java service(String) 设置 service 模板路径 /templates/service.java serviceImpl(String) 设置 serviceImpl 模板路径 /templates/serviceImpl.java mapper(String) 设置 mapper 模板路径 /templates/mapper.java mapperXml(String) 设置 mapperXml 模板路径 /templates/mapper.xml controller(String) 设置 controller 模板路径 /templates/controller.java new TemplateConfig.Builder() .disable(TemplateType.ENTITY) .entity("/templates/entity.java") .service("/templates/service.java") .serviceImpl("/templates/serviceImpl.java") .mapper("/templates/mapper.java") .mapperXml("/templates/mapper.xml") .controller("/templates/controller.java") .build(); #注入配置(InjectionConfig) 方法 说明 示例 beforeOutputFile(BiConsumer<TableInfo, Map<String, Object>>) 输出文件之前消费者 customMap(Map<String, Object>) 自定义配置 Map 对象 Collections.singletonMap("test", "baomidou") customFile(Map<String, String>) 自定义配置模板文件 Collections.singletonMap("test.txt", "/templates/test.vm") new InjectionConfig.Builder() .beforeOutputFile((tableInfo, objectMap) -> { System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size()); }) .customMap(Collections.singletonMap("test", "baomidou")) .customFile(Collections.singletonMap("test.txt", "/templates/test.vm")) .build(); #策略配置(StrategyConfig) 方法 说明 示例 enableCapitalMode 开启大写命名 默认值:false enableSkipView 开启跳过视图 默认值:false disableSqlFilter 禁用 sql 过滤 默认值:true,语法不能支持使用 sql 过滤表的话,可以考虑关闭此开关 enableSchema 启用 schema 默认值:false,多 schema 场景的时候打开 likeTable(LikeTable) 模糊表匹配(sql 过滤) likeTable 与 notLikeTable 只能配置一项 notLikeTable(LikeTable) 模糊表排除(sql 过滤) likeTable 与 notLikeTable 只能配置一项 addInclude(String...) 增加表匹配(内存过滤) include 与 exclude 只能配置一项 addExclude(String...) 增加表排除匹配(内存过滤) include 与 exclude 只能配置一项 addTablePrefix(String...) 增加过滤表前缀 addTableSuffix(String...) 增加过滤表后缀 addFieldPrefix(String...) 增加过滤字段前缀 addFieldSuffix(String...) 增加过滤字段后缀 entityBuilder 实体策略配置 controllerBuilder controller 策略配置 mapperBuilder mapper 策略配置 serviceBuilder service 策略配置 new StrategyConfig.Builder() .enableCapitalMode() .enableSkipView() .disableSqlFilter() .likeTable(new LikeTable("USER")) .addInclude("t_simple") .addTablePrefix("t_", "c_") .addFieldSuffix("_flag") .build(); #Entity策略配置 方法 说明 示例 nameConvert(INameConvert) 名称转换实现 superClass(Class<?>) 设置父类 BaseEntity.class superClass(String) 设置父类 com.baomidou.global.BaseEntity disableSerialVersionUID 禁用生成 serialVersionUID 默认值:true enableColumnConstant 开启生成字段常量 默认值:false enableChainModel 开启链式模型 默认值:false enableLombok 开启 lombok 模型 默认值:false enableRemoveIsPrefix 开启 Boolean 类型字段移除 is 前缀 默认值:false enableTableFieldAnnotationEnable 开启生成实体时生成字段注解 默认值:false enableActiveRecord 开启 ActiveRecord 模型 默认值:false versionColumnName(String) 乐观锁字段名(数据库) versionPropertyName(String) 乐观锁属性名(实体) logicDeleteColumnName(String) 逻辑删除字段名(数据库) logicDeletePropertyName(String) 逻辑删除属性名(实体) naming 数据库表映射到实体的命名策略 默认下划线转驼峰命名:NamingStrategy.underline_to_camel columnNaming 数据库表字段映射到实体的命名策略 默认为 null,未指定按照 naming 执行 addSuperEntityColumns(String...) 添加父类公共字段 addIgnoreColumns(String...) 添加忽略字段 addTableFills(IFill...) 添加表字段填充 addTableFills(List) 添加表字段填充 idType(IdType) 全局主键类型 convertFileName(ConverterFileName) 转换文件名称 formatFileName(String) 格式化文件名称 new StrategyConfig.Builder() .entityBuilder() .superClass(BaseEntity.class) .disableSerialVersionUID() .enableChainModel() .enableLombok() .enableRemoveIsPrefix() .enableTableFieldAnnotation() .enableActiveRecord() .versionColumnName("version") .versionPropertyName("version") .logicDeleteColumnName("deleted") .logicDeletePropertyName("deleteFlag") .naming(NamingStrategy.no_change) .columnNaming(NamingStrategy.underline_to_camel) .addSuperEntityColumns("id", "created_by", "created_time", "updated_by", "updated_time") .addIgnoreColumns("age") .addTableFills(new Column("create_time", FieldFill.INSERT)) .addTableFills(new Property("updateTime", FieldFill.INSERT_UPDATE)) .idType(IdType.AUTO) .formatFileName("%sEntity") .build(); #Controller策略配置 方法 说明 示例 superClass(Class<?>) 设置父类 BaseController.class superClass(String) 设置父类 com.baomidou.global.BaseController enableHyphenStyle 开启驼峰转连字符 默认值:false enableRestStyle 开启生成@RestController 控制器 默认值:false convertFileName(ConverterFileName) 转换文件名称 formatFileName(String) 格式化文件名称 new StrategyConfig.Builder() .controllerBuilder() .superClass(BaseController.class) .enableHyphenStyle() .enableRestStyle() .formatFileName("%sAction") .build(); #Service策略配置 方法 说明 示例 superServiceClass(Class<?>) 设置 service 接口父类 BaseService.class superServiceClass(String) 设置 service 接口父类 com.baomidou.global.BaseService superServiceImplClass(Class<?>) 设置 service 实现类父类 BaseServiceImpl.class superServiceImplClass(String) 设置 service 实现类父类 com.baomidou.global.BaseServiceImpl convertServiceFileName(ConverterFileName) 转换 service 接口文件名称 convertServiceImplFileName(ConverterFileName) 转换 service 实现类文件名称 formatServiceFileName(String) 格式化 service 接口文件名称 formatServiceImplFileName(String) 格式化 service 实现类文件名称 new StrategyConfig.Builder() .serviceBuilder() .superServiceClass(BaseService.class) .superServiceImplClass(BaseServiceImpl.class) .formatServiceFileName("%sService") .formatServiceImplFileName("%sServiceImp") .build(); #Mapper策略配置 方法 说明 示例 superClass(Class<?>) 设置父类 BaseMapper.class superClass(String) 设置父类 com.baomidou.global.BaseMapper enableMapperAnnotation 开启 @Mapper 注解 默认值:false enableBaseResultMap 启用 BaseResultMap 生成 默认值:false enableBaseColumnList 启用 BaseColumnList 默认值:false cache(Class<? extends Cache>) 设置缓存实现类 MyMapperCache.class convertMapperFileName(ConverterFileName) 转换 mapper 类文件名称 convertXmlFileName(ConverterFileName) 转换 xml 文件名称 formatMapperFileName(String) 格式化 mapper 文件名称 formatXmlFileName(String) 格式化 xml 实现类文件名称 new StrategyConfig.Builder() .mapperBuilder() .superClass(BaseMapper.class) .enableMapperAnnotation() .enableBaseResultMap() .enableBaseColumnList() .cache(MyMapperCache.class) .formatMapperFileName("%sDao") .formatXmlFileName("%sXml") .build();

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

用户登录
用户注册