准大二暑假前半部分的项目收获

这个暑假有幸能够作为一个学徒参与了某个与实际公司合作的项目的开发。虽然只负责了其中对于微信二维码支付部分的封装,其中大部分的内容实际上是对官方提供的sdk工具的开盖即食,但由于自己基础知识薄弱,开发经验缺少,导致这么一项简单的任务前前后后也花费了十几天时间。当然其中的收获也颇丰,主要填补了一些在实际项目中的规范操作上的不足,并且涉及到了好多自己以前没有去了解过的知识。暑假过半,趁一时无事,在此整理一下自己的收获。

某只狐狸第一次完成的商业项目实践记录

第一部分:结构设计

之前由于在六月份一个比赛项目制作文档的时候掌握了一点制作流程图的能力,没想到很快就派上用场了

在开始动工项目之前,先理解需求并绘制代码结构组成的流程图带来的帮助比我想象中要大。在实际操作中,有了流程图之后在编写代码时就是“照着图写”,而很少出现一时中断而要花费较长时间重新整理思路的情况。本次项目中最后的设计流程图如下:

主体流程图
轮询流程图
(事实上由于后期前端那边提出了回调的要求,导致以上的图并不代表最终结果,但差别不大)

第二部分:把API封装入util

本来此处还要利用feign或http的方式自行把接口地址写一套封装,但最后发现微信提供的sdk已经完成了这一项工作,所以这部分算是逃课了,之后有机会再去研究一下sdk里基于http的封装方式吧。

这一步主要流程就是写一套util方法,接收api所需要的参数,调用sdk已经封装好的接口地址访问方法,使用参数进行访问,并把所得的返回结果封装入bean。一下以其中一个较为经典的案例作为代表。

    public static WXmicroPayResultBean wxMicroPay(String body,
                                                  String out_trade_no,
                                                  String total_fee,
                                                  String spbill_create_ip,
                                                  String auth_code) throws Exception {
        try{
            Map<String,String> params = new HashMap<>();
            params.put("body",body);
            params.put("out_trade_no",out_trade_no);
            params.put("total_fee",total_fee);
            params.put("spbill_create_ip",spbill_create_ip);
            params.put("auth_code",auth_code);
            System.out.println(params);
            Map<String, String> newParams = wxPay.fillRequestData(params);
            Map<String,String> res = wxPay.microPay(newParams);
            WXmicroPayResultBean resultBean = new WXmicroPayResultBean(res);
            return resultBean;
        }catch (Exception e){
            e.printStackTrace();
            throw new Exception("running error");
        }

值得一提的是,接口地址需要的传入参数和返回参数实际上都要求是XML格式,微信sdk提供方法的里内置了转换的过程,又让我逃了一节课。另外关于返回值的封装新知道了一些小知识:

  • 即如果是用于程序内部参数传递的封装,一般写在bean层里,而如果是用来向前端返回的封装,一般写在model层里
  • 一般util允许有返回值,但service层不允许有,而使用把返回值作为参数传入的方法

除此之外,静态的util如果需要调用非静态且有Exception抛出的方法,当对方法所在的类进行实例化的时候,必须接收下一层抛上来的Exception,并且在catch的过程中也保持为静态。说起来有点抽象,代码实现如下:

    private static final WXPay wxPay;

    static {
        try {
            wxPay = new WXPay(new WXmicroPayUtil.WXPayConfigImpl(), false);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

以后出现@Autowired又用不了的情况就可以参考是不是上面的原因。

第三部分:service的建立

在本次项目中,service层的作用就是调用util的方法,把封装好的返回信息bean进行分析和处理,分类成不同的错误原因,通过回调、直接返回等方式返回给前端或者其他需要相关信息的后端业务。直到这次项目,我才明白到一点,即返回给前端的信息力求简洁明了,错误筛选处理什么的就是后端封装的工作,前端拿个状态码和 message就差不多了。

本次项目service的主要逻辑是,把返回结果分成三类,即订单支付成功、支付失败、支付中,并把相关信息直接返回给前端。而针对支付中的情况,要新开一个线程,反复调用订单查询的util,直到超时或者查到明确的结果,通过回调返回订单支付成功/失败的结果。这样做的好处就是前端无需等待就能第一时间得到返回,而不会让其他业务陷入超时的境地。

这里涉及到两个新知识点:新开线程回调,此处进行分别解释

  • 线程

我们可以通过一个叫做@Async的注解和其配套注解实现多线程的使用。

当然默认只使用一个@Async好像就能用,springboot好像会默认支持最大线程数为200,但也可以自行配置

可以自己写一个配置类如下↓,就能凭借注解@Async("WXTaskExecutor")使用该配置的线程池了

@Configuration
@EnableAsync
public class WXExecutorConfiguration {

    @Bean("WXTaskExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程数5
        executor.setCorePoolSize(5);
        //最大线程数500
        executor.setMaxPoolSize(5);
        //缓冲队列500
        executor.setQueueCapacity(500);
        //允许线程的空闲时间60秒
        executor.setKeepAliveSeconds(60);
        //线程池名的前缀
        executor.setThreadNamePrefix("WXPayAsync-");
        executor.initialize();
        return executor;
    }
}

这样的配置方法有一些使用要点,网络上有相关的整理,截图如下:

失效场景

对于其中第三点,在同一个类中,一个非异步方法调用另一个异步方法的问题,可以利用代理类的方法解决。本次项目中就出现了一个非异步的service要调用另一个异步service的情况,解决代码如下:

MDealRecordService mDRSProxy = (MDealRecordService) AopContext.currentProxy();
mDRSProxy.wxLoopTask(out_trade_no,dealRecord);

当前方法和异步方法都在类MDealRecordService中,而当前方法要在必要时调用异步方法wxLoopTask,所以可以先用以上的代码产生一个代理类实体,然后用该实体调用异步方法。

  • 回调

我们可以通过回调向指定的url传输指定的信息

(这个部分限于不专业的个人理解,只能用于辅助参考,以后可以在新增文章用于回调的深一步了解)

在本次项目中,回调的代码在经过层层封装前,其最核心的部分如下:

  public static RemoteService CommonRequestUtil(String serverContext){
        return Feign.builder()
                .options(new Request.Options(1000,3500))
                .retryer(new Retryer.Default(5000,5000,3))
                .encoder(new JacksonEncoder())
                .target(RemoteService.class,serverContext);
    }

以上代码中的serverContext值为要回调的url,利用返回值实例化一个remoteService,然后利用remoteService调用RemoteService类中的方法multiPlatformDealCallBack,而该方法具体内容又如下:

    //多形态交易回调
    @RequestLine("GET ?orderId={orderId}&orderType={orderType}&userId={userId}&payType={payType}&payCashes={payCashes}")
    String multiPlatformDealCallBack(@Param("orderId") String orderId,
                                     @Param("orderType") String orderType,
                                     @Param("userId") String userId,
                                     @Param("payType") String payType,
                                     @Param("payCashes") double payCashes);

然后就能把以上key和我们塞入的value传给该url

然而我现在还没看懂其中的原理,有可能与Feign有关,先挖个坑,和第二部分的封装逃课问题以后一起填。

注(个人理解):由于异步方法与原来的方法出于不同线程,故异步方法在需求上就不可能即时向调用它的方法返回信息。如果强行返回给某个方法的变量来接收,该异步方法会等待该方法返回而停止运作,这就等同于没有异步甚至浪费资源。就算使用结果作为参数的方法,经过本人测试也只会得到null。故异步方法一般会独自使用回调来向外传输信息。

第四部分:本地代码的移植与合并

这里总体过程就是在CODING里基于git的分支合并,本身不涉及新的技术,所以把这个模块用于整个项目的查漏补缺。毕竟这些问题也往往是保护分支的管理员在进行合并分支的审核过程中找到的。

  1. 关于自动生成的主键

如果一个类在数据库存储的过程中,其主键id采用的uuid或者是id自增的策略,那么这个类在产生对象时是还不会生成这个id的。即当你new出这个对象时,调用其getId()方法,得到的结果只会是null。

所以如果我们在后续的代码中需要用到这个对象的id,需要先进行一次save,数据库才能生成其主键。比较优雅的写法就如dealRecord = mDealRecordDao.save(dealRecord);这样这个对象就有id了。

  1. 关于一些代码规范

主要要注意的一点就是关于魔法值的封装,甚至是一些像0、1、2这样的状态码也应该给予相应的封装。据说《阿里巴巴Java开发手册(黄山版)》对这些规范有详细的记载,以后可以看看(咕咕)。

此外在合并的过程中被提出的问题数不胜数,但今天要保持健康作息,就先写到这里吧。有什么需要补充的之后再更新

​ 2022-7-29 23:34



   转载规则


《准大二暑假前半部分的项目收获》 狐狸狐涂 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
LeetCodeDay1 LeetCodeDay1
初见LeetCode,题目整理 由于之前鸽了,本次整理包括的内容题目较多,虽然普遍不算难 内含:1480、383、412、876、1342、1672、1403、623、1 1480. 一维数组的动态和 当时我的第一想法是再新建一个大小
下一篇 
day2_web开发 day2_web开发
​ 尝试坚持每天学一点中~ Web开发1. 静态资源的配置 只要把静态资源文件放在/ststic(or /public or /resourceor /META-INF/RESOURCE)的路径下,在web端就可以直接利用url
2022-06-29
  目录