博客

  • 芋道代码解析-后端篇(0)

    江畔何人初见月?江月何年初照人?

    人生代代无穷已,江月年年望相似。

    不知江月待何人,但见长江送流水。

    ——唐·张若虚《春江花月夜》


    先不看所有的租户管理、安全管理和框架部分,先从一个完整的前端请求链路来看芋道中一个最典型的请求的完整后端行为。

    Controller 部分

    Controller 监听来自客户端的请求,也就是来自前端的 HTTP 请求(确切地说,是 Restful API 请求)。

    @Tag(name = "管理后台 - 商品 SPU")
    @RestController
    @RequestMapping("/product/spu")
    @Validated
    public class ProductSpuController {
        @Resource
        private ProductSpuService productSpuService;
        @Resource
        private ProductSkuService productSkuService;
    ...
        @GetMapping("/get-detail")
        @Operation(summary = "获得商品 SPU 明细")
        @Parameter(name = "id", description = "编号", required = true, example = "1024")
        @PreAuthorize("@ss.hasPermission('product:spu:query')")
        public CommonResult<ProductSpuRespVO> getSpuDetail(@RequestParam("id") Long id) {
            // 获得商品 SPU
            ProductSpuDO spu = productSpuService.getSpu(id);
            if (spu == null) {
                return success(null);
            }
            // 查询商品 SKU
            List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
            return success(ProductSpuConvert.INSTANCE.convert(spu, skus));
        }
    ...

    首先定义该类为一个 RestController,便于 Spring Boot 扫描、注册、进行管理,接着定义该 Controller 管理的全局请求的路由连接 /product/spu,其他的注解大多是用于团队协作的文档型注解。

    接着,启动 Service 实例。我们要用到的大部分业务代码,都会通过该实例进行调用,从而隐藏了具体的业务逻辑细节,实现了解耦的目的。解耦使代码更加易于团队协作和维护。

    @RestController
    @RequestMapping("/product/spu")
    public class ProductSpuController {
        @Resource
        private ProductSpuService productSpuService;
        @Resource
        private ProductSkuService productSkuService;
    ...
    }

    随后,我们来看一个具体实现的 Controller 类方法。以 getSpuDetail 为例。首先,该方法的核心注解 @GetMapping 定义了具体的路由 /get-detail,随后注解 @PreAuthorize 则检查了用户的具体权限,作了安全的权限处理,如果存在问题则会直接拦截,不再继续方法,返回给前端对应的错误。

    在具体的方法定义中,我们可以看见,方法 getSpuDetail 的输出是 CommonResult<T>,这里的 T 是 ProductSpuRespVO,是一个专门定义过的用于前端展示的视图对象(View Object),接受的输入则是经过注解的@RequestParam Long id。

    关于 @RequestParam

    在一般的 java 方法获取输入时,我们只需要写输入的类型和变量名就好。为什么这个方法要在输入中使用注解呢?因为在这里,我们的 Controller 获得的请求实际上是一个完整的 HTTP 请求,而具体到请求的资源路径,也是一个很长的 URL,而不是一个具体的 java 类型的变量。

    因此,注解 @RequestParam 对该 URL 进行了解析,根据方法定义的变量名找出对应的 key-value(本方法中,也就是 id),并对原本是 String 类型的 value 进行转换,转换为方法定义的类型 Long。如果转换失败,则会在这里进行 return 返回,而不是继续执行程序。

    于是做完所有的工作后,方法 getSpuDetail 接收到的输入就是一个 Long 类型的具体数值了。

    通过调用 Service 实例的 getSpu 方法,我们获得了一个 ProductSpuDO 类型的 spu 变量。在这里,我们需要有一个信念,即我相信这个方法返回给我的是一个正常正确的值,这也是分层的意义所在,每一层做好自己负责的工作内容,为其他层提供对应的服务并保证可靠的交付成果。

    随后,如果该 spu 变量存储的对应数据信息是不为空的,也就是有正常存储内容的,继续执行该方法;如果为空,则直接返回以 success 方法处理空值的结果,因为在与前端的交互中,我们是以 Restful API 的形式交互的,肯定不能只返回一个 null 值——这在我们的方法定义中也有表现,返回的是一个 CommonResult<T> 结果。

    success 函数具体实现
    public static <T> CommonResult<T> success(T data) {
    CommonResult<T> result = new CommonResult<>();
    result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
    result.data = data;
    result.msg = "";
    return result;
    }

    接下来的逻辑也大体相同。刚才我们获得了 spu 的具体内容,随后通过 Sku Service 实例调用对应的方法,查询该 spu id 对应的具体明细,获得对应数据结构类型 List<ProductSkuDO> 的具体数据结果,存储在 skus 变量中,并调用 success 方法进行返回具体获取的 data 内容。

        @GetMapping("/get-detail")
        @Operation(summary = "获得商品 SPU 明细")
        @Parameter(name = "id", description = "编号", required = true, example = "1024")
        @PreAuthorize("@ss.hasPermission('product:spu:query')")
        public CommonResult<ProductSpuRespVO> getSpuDetail(@RequestParam("id") Long id) {
            // 获得商品 SPU
            ProductSpuDO spu = productSpuService.getSpu(id);
            if (spu == null) {
                return success(null);
            }
            // 查询商品 SKU
            List<ProductSkuDO> skus = productSkuService.getSkuListBySpuId(spu.getId());
            return success(ProductSpuConvert.INSTANCE.convert(spu, skus));
        }

    Service 部分

    接下来我们谈论 Service 部分。以 ProductSpuService 为例。

    首先我们来到源码部分。

    当我们进入 ProductSpuService.java 文件的时候,会发现它是一个定义的接口。这是一种设计模式——面向接口编程。在该接口中,我们定义了我们所需的所有方法——我们相信这些方法是任何实现接口时必须实现的核心方法——换句话说,我们认为,实现这个 Service 服务,必须要有这些方法的实现。因此我们预先做了定义。这是一种非常规范的架构设计思维。

    值得一提的是,如果在像 Java 这样的强类型语言中,接口定义的方法没有被实现,那么在编译阶段编译器就会出现报错。

    public interface ProductSpuService {
        /**
         * 获得商品 SPU
         *
         * @param id 编号
         * @param includeDeleted 是否包含已删除的
         * @return 商品 SPU
         */
        ProductSpuDO getSpu(Long id, boolean includeDeleted);
    }

    对应的具体实现,在 ProductSpuServiceImpl.java 文件中。在这里,我们可以看到,该类 ProductSpuServiceImpl 是接口(interface)的一个实现(implement)。

    在具体的实现上,首先讨论注解。@Service 注解告诉 Spring Boot,该类是一个 Service。

    @Service
    @Validated
    public class ProductSpuServiceImpl implements ProductSpuService {
    
        @Resource
        private ProductSpuMapper productSpuMapper;
    
        @Resource
        @Lazy // 循环依赖,避免报错
        private ProductSkuService productSkuService;
        @Resource
        private ProductBrandService brandService;
        @Resource
        private ProductCategoryService categoryService;
    ...
    }

    我们详细看 Controller 中实例调用的 getSpu()方法的实现。

  • 谈古法编程

    现在关于编程的学习路径,人们常常众说纷纭。

    有人认为,现在 AI Coding 如此发达,编程语言在很大程度上已经不是门槛了,你只需要对 AI 表述你的需求,使用恰当的 prompt 作为规范,那么总是会有不错的结果——那不仅比作为初学者的你写得好得多,而且效率高得多!如果结果不好,那么大概率是这个模型令人遗憾。

    也有人认为,不对,使用 AI 间接生成代码而不是自己去推敲——也就是现在涌现的新名词古法编程(Hand-Coding)——会导致知识与经验的断层,对于一门定义规范严谨的学科来说,从长远看来是不可持续的。

    这里,我就讲对于初学者,我的看法和意见。


    以 Java 为例。

    虽然许多人现在唱衰 Java,认为学习它对于目前的就业是非常没有前途的,但那仅仅是局限于职业场景,而且是对于初级工程师的范围。但是毫无疑问,这是一门生态非常完善的语言,逻辑定义也非常自洽。那么为什么不建议呢?因为 Java 实际上是一门很年轻的语言,它诞生刚刚 31 年,而绝大多数有经验的工程师也恰好是这个年纪上下,也就是说,有大量的人才擅长于使用这一门语言,这在任何职业场景中对于初级新人来说都是不友好的。但是这与它的自身特质并没有什么关系。

    有人说,AI 是相当擅长写项目和代码的。当然!这是因为大量的程序员贡献了他们的开源成果,而代码本身就是有逻辑结构严格组织的,因此是易于 AI 学习的。那么,AI 几分钟内就能生成的项目,是否意味着计算机新人已经毫无用处——公司为什么要招聘效率不高的新人,而不去使用全知全能的 AI 呢?

    首先,一个项目开发是非常长的周期。需要经过不断的开会讨论、需求调研整改、评审评估,这并不是很快能够搞定的。个人开发者几分钟内生成的 javascripts 网站代码、一个 demo 的完整项目,在真正的社会考验中是非常脆弱的。因为在社会环境中,公司开发一个项目最终的目的就是面向客户发售,获取利润,此外没有任何意义。面对用户的需求,这个项目的优势在哪里?安全性如何?稳定性如何?如何支持高并发?为什么选择你的而不是别人的?每一个问题都需要往底下深入研究和讨论,而这是那些小玩具所很难实现的,因为全权由 AI 生成的代码,大多数情况下我们是无从把握的,我们并不清楚完整的架构、具体的实现细节、而那庞大的生产量也令这些的审查有时候难以进行。如果 AI 真的如此发达,那么小公司如今很多情况下可以不必购买互联网公司提供的平台和其他服务,毕竟 AI 的成本远远低于每年购买这些软件的钱——我干嘛不自研呢?——可以试试,但是总是会搞得一团混乱的。代码是为目的服务的,而不是为了其本身而服务的。

  • 沉沦与存在

    ⌈人是被抛到这个世界上来的。⌋

    ——德·海德格尔《存在与时间》

    每每行走在这个世界上,我都会感到自身的渺小!时代发展就像钢铁巨兽,它以一种前所未有的毁灭的姿态,会碾碎所有试图在前进道路上做出微小阻拦的个体。我不知道这种描述是否是准确的,但在我的主观世界里,这个时代给我的印象就是这样滚烫而冰冷。

    尽管现代社会,在我们所处的国家,根据《民法典》 18 岁就已经是完全民事能力行为人,但不知为何,常常我仍感受到自己的年轻和脆弱。那种发自内心的不安全感是无法掩盖和隐藏的。在社会中我并没有找到自己的位置,对于未来我没有明确的保障,技术的发展使我对自我的价值焦虑不已而无法专注于当下的任务,于是对于生存的恐惧像一颗种子一样扎根于心中,最终在躯壳中成为落下厚重阴影的大树。

    猛兽以爪牙攻击、猎杀,不屈地对抗着自然界,与无数竞争者争夺资源——潜伏、寄生、合作、止损——那是最直观、最残酷的搏斗,因为输家只有一个下场。人类生存在组织构建出的形而上的社会,资源透过层层叠叠的社会约定进行交易,更加体面、安全,不必面临血肉苦痛,因为我们的物种已经凌驾于压迫别的物种之上;而在社会之内,层级的分配仍然同样如此。

    哎!我的天啊!谁来看看这个世界!我发现,在无聊、混沌、迷茫中,我对于自身的感知会更加清晰,大脑好像在不断起伏挤压着颅骨,于是强烈的情感和欲望便更加令人无法忍受了。然而平时不是这样的,我感到无耻和卑鄙,我对自我感到羞愧,但是我甚至不知道这种无助是从何而来的。

    为什么感到不幸?为什么感到无聊?但我也并不想要追求疯狂。保守的社会倾向让我立于高地看着混乱的人群,尽管我并不更加高尚!


    现在,我再次写下这些话,我的情绪稳定了很多。这仅仅是到了一天快要结束的时间,也许是因为我吃了我最喜欢的泡椒凤爪,它有清脆爽辣的口感。在事实上我的生活并没有发生任何实质性的巨大的改变,但是此刻我的内心却得到了一种奇异的宁静,这简直不可思议。但我想那也仍然是有些不满足的。因为时过不久,到太阳升起的时候,我的胃会再一次出现难忍的漫长的空洞。那是生物研究中名为本能的东西。


    RedditSlayer2020

    我真的很感谢你这么认真又长的回答,但是我完全不同意。当哲学家本身就意味着你有时间和脑力去思考,这是80%的人类所没有的,因为他们忙着维持生计,养家糊口,或者仅仅是生存。看看东南亚,大多数人都在为发达国家的利益而辛苦劳作。这没有意义,这是一个虚构的概念,基本上是幻觉,让人们从残酷的现实中分心。当然有目的。人类动物的目的就是存在和繁殖。我们是自然循环的一部分,但我们已经与自然脱节,留下了一个需要填补的空虚……直到自然淘汰我们。

    https://www.reddit.com/r/Existentialism/comments/1cei2fx/man_is_condemned_to_be_free_because_once_thrown/?tl=zh-hans

  • 醒来吧,朋友!走出山洞吧!

    ⌈……慢慢地,他的眼睛适应了太阳光线。起初他只能看到影子,后来逐渐可以看到水中人和物体的倒影,然后再看到人和物体本身。他能够在晚上看到星星和月亮,最终可以看到太阳本身。⌋

    ——柏拉图《理想国(512b)》

     你知道的,现在 AI 趋势 算是 所有人都能看见的了。 虽然我先前总是告诉自己,没事的,肯定还有我的岗位的,但是我现在发现我不能装作没有看见房间里的大象了。我曾经觉得, AI 做的事情是不准确的,我们不能保证它的可靠性,而在我的内心我边缘了这个技术,但是我看到很多大的公司都在拼命做这个技术,花了很大的价钱去研究这个,我就在想:那么是什么值得它们去花那么多价值去研究这些呢?如果觉得 AI 是黑盒,但其实人也是黑盒,人学习的逻辑和知识也是从无到有的,所以我并不能盲目地说 AI 就是不可靠的,因为 人也会不可靠, 发生错误、异常都是非常正常和自然的事情。 我难道是技术保守派,对新的进步的东西感到不安,而不去狂热地接触和理解它们吗?于是我告诉自己,醒来吧,走出山洞吧!AI 的数据库是人类的千百年来的经验和知识,它真的就像一个可塑造的客体——学会语言而与我们对话的客体——也许我们正走在一个充满奇幻色彩的道路上。

  • 世界,您好!

    Hey,希格斯!这是您的第一篇文章。编辑或删除它,然后开始写作吧!