精华内容
下载资源
问答
  • 在这里,统一封装微信支付支付宝支付的API,使两种支付方式对外提供一致的调用接口。    首先我们来看支付宝支付SDK的API怎么设计的: /** * 支付接口 * * @param orderStr 支付订单信息字串 ...

      开发过支付宝、微信支付的同学都知道,微信的支付 API 设计感觉是 Java 开发工程师写的,远不如支付宝 SDK 的接口设计用起来顺手。在这里,统一封装微信支付和支付宝支付的API,使两种支付方式对外提供一致的调用接口。

       首先我们来看支付宝支付SDK的API是怎么设计的:

    /**
     *  支付接口
     *
     *  @param orderStr        支付订单信息字串
     *  @param schemeStr       调用支付的app注册在info.plist中的scheme
     *  @param completionBlock 支付结果回调Block,用于wap支付结果回调
                               跳转支付宝支付时只有当processOrderWithPaymentResult接口的completionBlock为nil时会使用这个bolock
     */
    - (void)payOrder:(NSString *)orderStr
          fromScheme:(NSString *)schemeStr
            callback:(CompletionBlock)completionBlock;
    

      支付宝SDK的设计十分简洁,只有三个参数,分别负责:1.支付信息 2.进程跳转的 scheme 3.支付结果回调 block。

      然后看一下微信支付的API:

    /*! @brief 发送请求到微信,等待微信返回onResp
     *
     * 函数调用后,会切换到微信的界面。第三方应用程序等待微信返回onResp。微信在异步处理完成后一定会调用onResp。支持以下类型
     * SendAuthReq、SendMessageToWXReq、PayReq等。
     * @param req 具体的发送请求,在调用函数后,请自己释放。
     * @return 成功返回YES,失败返回NO。
     */
    + (BOOL)sendReq:(BaseReq*)req;
    /*! @brief 处理微信通过URL启动App时传递的数据
     *
     * 需要在 application:openURL:sourceApplication:annotation:或者application:handleOpenURL中调用。
     * @param url 微信启动第三方应用时传递过来的URL
     * @param delegate  WXApiDelegate对象,用来接收微信触发的消息。
     * @return 成功返回YES,失败返回NO。
     */
    + (BOOL)handleOpenURL:(NSURL *)url delegate:(nullable id<WXApiDelegate>)delegate;
    

      要发起微信支付,首先要实例化 一个PayReq对象,接着将这个对象的6个字段分别赋值,然后发起支付请求,等从微信返回到APP后,再从代理方法获取支付结果。相比支付宝SDK提供的接口,要繁琐了不少,可见接口设计是多么的重要,(当然支付宝的入参是事先拼接好的,拼接工作可以移交给后端)。

      我们希望不管是微信支付还是支付宝支付,都能有统一风格的调用接口,方便逻辑复用,那么接下来的工作就是通过封装 使这这两者的接口统一。

    在这里,我们设想的接口是:

    1. 只有一个入参,组织参数的方式尽量简单
    2. 回调直接给出结果

    以下是设计的接口:

    typedef void(^PayResult)(PayResponse *result);
    typedef void(^wxShareCallback)(int resp);
    @interface PaymentHub : NSObject<WXApiDelegate>
    /**
     获取 单利
     @return 返回单利实例
     */
    + (instancetype)hub;
    /**
     支付宝支付
     @param orderStr 支付订单加密信息
     @param callback 支付结果回调
     */
    - (void)AlipayWithOrder:(NSString *)orderStr callback:(PayResult)callback;
    
    /**
     微信支付
     @param request 微信支付所需参数
     @param callback 支付结果回调
     */
    - (void)WXpayWithRequest:(PayReq *)request callback:(PayResult)callback;
    
    /**
     进程间通信处理
     @param url 进程间 交换的数据
     @param callback 微信分享回调
     */
    - (void)handleOpenUrl:(NSURL *)url response:(wxShareCallback)callback;
    + (PayReq *)fromMap:(NSDictionary *)dict;
    

    为了方便组织微信支付所需的字段,fromMap方法可以将包含数据的字典转换为 一个PayReq实例对象。在封装里,将微信SDK的 代理指定为 PaymentHub的实例,在实例内部处理微信的代理回调。
    考虑到微信SDK还可能会负责分享,在handleOpenUrl方法里需要做区分处理。

    demo链接

    展开全文
  • 在这里,统一封装微信支付支付宝支付的API,使两种支付方式对外提供一致的调用接口。    首先我们来看支付宝支付SDK的API怎么设计的: /** * 支付接口 * * @param orderStr 支付订单信息字串 * @...

      开发过支付宝、微信支付的同学都知道,微信的支付 API 设计感觉是 Java 开发工程师写的,远不如支付宝 SDK 的接口设计用起来顺手。在这里,统一封装微信支付和支付宝支付的API,使两种支付方式对外提供一致的调用接口。

       首先我们来看支付宝支付SDK的API是怎么设计的:

    /**
     *  支付接口
     *
     *  @param orderStr        支付订单信息字串
     *  @param schemeStr       调用支付的app注册在info.plist中的scheme
     *  @param completionBlock 支付结果回调Block,用于wap支付结果回调
                               跳转支付宝支付时只有当processOrderWithPaymentResult接口的completionBlock为nil时会使用这个bolock
     */
    - (void)payOrder:(NSString *)orderStr
          fromScheme:(NSString *)schemeStr
            callback:(CompletionBlock)completionBlock;
    
    

      支付宝SDK的设计十分简洁,只有三个参数,分别负责:1.支付信息 2.进程跳转的 scheme 3.支付结果回调 block。

      然后看一下微信支付的API:

    /*! @brief 发送请求到微信,等待微信返回onResp
     *
     * 函数调用后,会切换到微信的界面。第三方应用程序等待微信返回onResp。微信在异步处理完成后一定会调用onResp。支持以下类型
     * SendAuthReq、SendMessageToWXReq、PayReq等。
     * @param req 具体的发送请求,在调用函数后,请自己释放。
     * @return 成功返回YES,失败返回NO。
     */
    + (BOOL)sendReq:(BaseReq*)req;
    /*! @brief 处理微信通过URL启动App时传递的数据
     *
     * 需要在 application:openURL:sourceApplication:annotation:或者application:handleOpenURL中调用。
     * @param url 微信启动第三方应用时传递过来的URL
     * @param delegate  WXApiDelegate对象,用来接收微信触发的消息。
     * @return 成功返回YES,失败返回NO。
     */
    + (BOOL)handleOpenURL:(NSURL *)url delegate:(nullable id<WXApiDelegate>)delegate;
    
    

      要发起微信支付,首先要实例化 一个PayReq对象,接着将这个对象的6个字段分别赋值,然后发起支付请求,等从微信返回到APP后,再从代理方法获取支付结果。相比支付宝SDK提供的接口,要繁琐了不少,可见接口设计是多么的重要,(当然支付宝的入参是事先拼接好的,拼接工作可以移交给后端)。

      我们希望不管是微信支付还是支付宝支付,都能有统一风格的调用接口,方便逻辑复用,那么接下来的工作就是通过封装 使这这两者的接口统一。

    在这里,我们设想的接口是:

    1. 只有一个入参,组织参数的方式尽量简单
    2. 回调直接给出结果

    以下是设计的接口:

    typedef void(^PayResult)(PayResponse *result);
    typedef void(^wxShareCallback)(int resp);
    @interface PaymentHub : NSObject<WXApiDelegate>
    /**
     获取 单利
     @return 返回单利实例
     */
    + (instancetype)hub;
    /**
     支付宝支付
     @param orderStr 支付订单加密信息
     @param callback 支付结果回调
     */
    - (void)AlipayWithOrder:(NSString *)orderStr callback:(PayResult)callback;
    
    /**
     微信支付
     @param request 微信支付所需参数
     @param callback 支付结果回调
     */
    - (void)WXpayWithRequest:(PayReq *)request callback:(PayResult)callback;
    
    /**
     进程间通信处理
     @param url 进程间 交换的数据
     @param callback 微信分享回调
     */
    - (void)handleOpenUrl:(NSURL *)url response:(wxShareCallback)callback;
    + (PayReq *)fromMap:(NSDictionary *)dict;
    
    

    为了方便组织微信支付所需的字段,fromMap方法可以将包含数据的字典转换为 一个PayReq实例对象。在封装里,将微信SDK的 代理指定为 PaymentHub的实例,在实例内部处理微信的代理回调。
    考虑到微信SDK还可能会负责分享,在handleOpenUrl方法里需要做区分处理。

    demo链接

    展开全文
  • 1. mybatis的架构流程分析 先直接来一张mybatis的架构图,看下mybatis按照模块...这层主要考虑的就是使用人的需求,用起来怎么方便怎么设计,属于对外接口。 数据处理层 数据处理层就是对内的,整个数据处理层...

    本章希望通过手写mybatis框架的核心部分,帮助加深对mybatis框架底层原理的理解,主要包括框架的执行流程,动态sql的解析流程,框架中各个组件和类的作用,以及他们之间是如何交互的。
    手写之前,我们需要对mybatis的架构和流程有大致的掌握。

    1. mybatis的架构和流程分析

    先直接来一张mybatis的架构图,看下mybatis按照架构整体是怎么设计的:
    在这里插入图片描述

    • 接口层
      接口层是MyBatis提供给开发人员的一套API,主要表现在SqlSession接口,通过SqlSession接口开发人员就可以完成对数据库CRUD。这层主要考虑的就是使用人的需求,用起来怎么方便接口就怎么设计,属于对外的接口。

    • 数据处理层
      数据处理层就是具体解析和执行sql的地方,是MyBatis框架内部的核心,底层最终还是调用JDBC代码来执行的。这层就是通过对JDBC执行sql的步骤进行了拆解,划分了不同模块,同时抽象出了很多组件,每个组件各司其职,通过协作让整个sql执行更加灵活。
      主要责任:
      (1)参数的解析与绑定
      (2)SQL的解析
      (3)SQL的执行
      (4)结果集映射的解析与处理

    • 基础支撑层
      支撑层用来支撑整个框架运行的,主要负责:
      (1)对数据库连接的支持与管理
      (2)对事务的支持与管理
      (3)对配置信息的支持与管理
      (4)对查询缓存的支持与管理


    接着我们看下执行流程图,看下配置文件是如何结合Mybatis中各个组件进行整个程序执行的。
    在这里插入图片描述

    说明

    • 1.mybatis配置文件
      所有的配置文件在框架运行的一开始就会被解析,并将所有的配置信息封装到Configuration对象中。

      • SqlMapConfig.xml,全局配置文件,配置了mybatis的运行环境等信息。
      • Mapper.xml,映射文件,配置了sql相关信息。
    • 2.SqlSessionFactory
      通过mybaris环境等配置信息构造SqlSessionFactory,即会话工厂。(创建过程主要是通过SqlSessionFactoryBuilder对象加载配置文件,将解析的配置信息封装成Configuration对象后,再将Configuration对象传递给SqlSessionFactory对象完成构建)

    • 3.sqlSession
      通过上面的会话工厂创建sqlSession会话,程序员就是通过sqlsession会话接口对数据库进行增删改查操作。这个是mybatis对外提供的操作接口。

    • 4.Executor执行器
      mybatis底层定义了Executor执行器接口来具体操作数据库, 而上面的对外接口sqlsession底层就是通过executor接口操作数据库的。Executor接口有两个实现,一个是基本执行器BaseExecutor(是一个抽象类,采用模板方法设计模式,基本执行器下面又分为简单执行器、可重用执行器、批量执行器…)、一个是缓存执行器CachingExecutor,Executor是mybatis内部的执行接口,四大组件之一。
      (四大组件:Exucutor、StatementHandler、ParameterHandler、ResultSetHandler,各司其职,让SQL执行流程更加灵活)

    • 5.MappedStatement
      它也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql映射信等。映射文件中每一个select\insert\update\delete标签都对应一个Mapped Statement对象,select\insert\update\delete标签的id即是Mapped statement的id,此外还封装了标签中的sql语句(SqlSource),参数类型,结果集类型,statment类型等。(JDBC中,有三种Statement类型,Statement,PreparedStatment,CallableStatment)

      • MappedStatement对sql执行的输入参数进行定义,支持HashMap、基本类型、pojo,Executor在MappedStatemen执行sql前将输入的java对象映射至sql中,输入参数映射过程相当于jdbc编程中对preparedStatement设置参数。

      • MappedStatement对sql执行的输出结果进行定义,支持HashMap、基本类型、pojo,Executor在MappedStatemen执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。

    在这里插入图片描述


    接下来看第三张图,主要讲解的是SqlSession水平面下的调用过程,展示了整个调用过程中各个组件的职责,以及组件间是如何交互,如何和JDBC的代码进行交互的:
    在这里插入图片描述

    • SqlSession:可以看到SqlSession作为MyBatis工作的顶层API接口,是对外提供的操作接口,而底层是调用Executor执行器。

    • Executor:执行器是Mybatis的核心,是内部的执行接口,同时还提供了查询缓存的功能。

    • SqlSource:MappedStatment中的SqlSource具有解析sql的功能,通过SqlSource可以并获得BoundSql,它维护了可执行的Sql语句和对应的参数映射信息。

    • StatementHandler:StatementHandler是用来处理和JDBC中的Statement进行交互的,例如对statment设置入参、执行查询、执行更新、结果集封装等。

    • ParameterHandler:StatementHandler对statment设置入参,底层主要依靠这个接口。

    • ResultSetHandler:StatementHandler对statment的结果集封装,底层主要依靠这个接口。

    通过上面的分析,我们进行一个总结:

    • SqlSession
      mybatis对外提供的程序员使用的接口,通过接口完成CRUD
    • Executor
      执行器,mybatis内部执行的接口,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
    • StatementHandler
      封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
    • ParameterHandler
      负责对用户传递的参数转换成JDBC Statement 所需要的参数
    • ResultSetHandler
      负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
    • TypeHandler
      负责java数据类型和jdbc数据类型之间的映射和转换
    • MappedStatement
      维护了一条<select|update|delete|insert>节点的封装
    • SqlSource
      负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回BoundSql,表示动态生成的SQL语句以及相应的参数信息
    • Configuration
      MyBatis所有的配置信息都维持在Configuration对象之中。

    2.Mybatis中的SQL解析

    上面已经提到过SqlSource具有解析sql的功能,那么解析流程到底是什么样的呢?我们举个例子:
    在这里插入图片描述
    之前说过MappedStatement对象维护了一条<select|update|delete|insert>节点的封装,上面的例子中,整个<select>就对应一个MappedStatement对象,里面维护了id,parameterType,resultType,statmentType和标签内的sql语句。

    那么标签内的sql语句具体是以什么样的数据结构存储的呢?
    首先分析一下<select>标签,对于xml来说,它是一个混合元素,可以包含子标签,并且子标签之间可以穿插文本内容,dom解析<select>标签就可以看成一个Node节点,它有很多子节点,子节点中既有文本节点,也有元素节点,元素节点又有可能有子节点…而在mybatis中把这样的节点,定义成了【SqlNode】,直译为Sql节点。【SqlNode】它是一个接口,按照节点包含的内容不同,实现类分为:

    • StaticTextSqlNode:封装的是仅带有#{}的文本节点
    • TextSqlNode:封装的是带有${}的文本节点
    • IfSqlNode:封装的是if动态标签的混合节点
    • MixedSqlNode:混合节点,代表一组节点。使用组合设计模式,以集合方式存储子节点,使得用户可以使用一致的方法操作单个对象和组合对象。

    所以【SqlNode】主要就是用来存储<select>标签下一个个sql节点的。除了存储以外,<select>标签所代表的sql语句还需要根据用户传递的参数对其包含的一个个sql节点进行选择拼接,最终拼成一个完整的sql语句。

    因此【SqlNode】接口定义了一个方法apply(DynamicContext context),其中DynamicContext里面维护了用户传递的入参,和一个StringBuilder变量,用来存储拼接的sql语句,该方法会根据当前节点的逻辑对节点中的信息进行处理,然后拼接到StringBuilder中,注意此时的处理,只是根据动态标签的逻辑,完成了字符串的拼接,或者替换了字符串中的${},并不会处理#{}。当调用顶层节点的apply方法的时候,会依次遍历树中每个节点,所以每个节点都有机会并且按照顺序依次执行自己的apply方法对StringBuilder进行拼接,最终从StringBuilder中方可以得到一个完整的sql语句,当然得到的是一个最多只包含#{}的sql语句。

    上面的过程我们可以称为拼接过程

    只包含#{}的sql语句,还不能被JDBC直接执行,所以mybatis定义了一个SqlSource接口,其实现类会包含SqlNode,并提供了一个获取可执行SQL的方法getBoundSql(Object parameterObject),该方法底层实现,会调用解析器sqlSourceParser,它会根据用户传递的入参,将SqlNode拼接完整的语句进行一次解析,把#{}替换为?占位符,同时将#{}中的参数信息封装成了【ParameterMapping】对象,组成一个集合按照顺序作为该sql语句占位符对应的参数映射信息,最后将sql和参数信息封装到了【BoundSql】对象返回。

    这个过程我们可以称为解析过程

    根据【SqlSource】实现类中包含的【SqlNode】类型不同,也有不同实现类:

    • DynamicSqlSource:包含了带有${}或者动态SQL标签的SqlNode
    • RawSqlSource:仅包含只带有#{}的SqlNode

    为什么这么分?因为只包含#{}的sql语句,是不需要经历拼接过程的(没有${}或者动态标签需要处理),只需要解析一次以后,就可以使用占位符来长期使用。而${}和动态标签的sql,每次用户传递的入参不同,整个语句拼接后的结果都不一样,所以每次调用都需要重新拼接并解析。
    所以往往RawSqlSource在构造的时候拼接解析就可以了,而DynamicSqlSource要在每次调用方法的时候重新拼接在解析

    • StaticSqlSource:上面两个SqlSource解析完成以后的结果都会封装到该对象中,这里面的sql是可以直接使用的。
    总结SQL解析:

    SQL解析可以分成两个阶段:

    • 拼接阶段:拼接sql节点,只处理动态标签和${},最终返回只包含#{}的sql语句
    • 解析阶段:处理#{},解析成占位符,最终返回可执行的sql和参数映射信息

    涉及的接口和类:

    • SqlSource接口:
      第一、实现类要封装未解析的SQL信息(SqlNode集合)
      第二、要提供对未解析的SQL信息,进行解析的功能

      • DynamicSqlSource:封装的SqlNode集合中含有"带有${}或者动态SQL标签的SqlNode"
        解析工作是发生在每一次调用getBoundSql方法的时候
      • RawSqlSource:封装的SqlNode集合中只含有"只带有#{}的SqlNode"
        解析工作是发生在第一次构造RawSqlSource的时候,只需要被解析一次
      • StaticSqlSource:当上面两个SqlSource解析后,都会把解析结果封装到该对象中,通过该SqlSource获取到BoundSql
    • SqlNode接口:
      1、封装SQL节点信息
      2、提供对封装的SQL节点的拼接解析功能,此处解析指替换#{}

      • MixedSqlNode:主要起到封装集合节点作用,并且进行一些操作,代表的含有子节点的节点
      • TestSqlNode:封装的是带有${}的文本字符串,并提供根据参数替换掉${}功能
      • StaticTextSqlNode:封装的是带有#{}的文本字符串,不用特殊处理
      • IfSqlNode:封装的是if动态标签的文本字符串,根据参数决定是否拼接

      • 元素:我们认为动态标签和${}都是字符串拼接的操作,对于这种操作都认为是动态的sql信息,#{}或者没有占位符都属于静态,对应sql处理"拼接"和"解析"两个流程
    • DynamicContext:动态上下文,就是封装的入参信息,解析过程中的SQL信息

    • BoundSql:封装解析之后可以执行的sql语句,以及解析占位符?时产生的参数映射信息

    • ParameterMapping:从#{}中解析出来的参数信息,包括参数名称和类型

    SqlSourece、SqlNode、BoundSql这几个接口在整个解析阶段和执行阶段都非常重要!也是最难理解的部分。

    3.分析JDBC的执行步骤

    mybatis底层都是对JDBC的封装,接下来我们在JDBC的执行步骤基础上简单分析一下如何手写mybatis,主要以面向过程方式分析:

    	@Test
        public void test() {
            Connection conn = null;
            PreparedStatement stmt = null;
            ResultSet rs = null;
    
            try {
                // 1 注册驱动
                Class.forName("com.mysql.jdbc.Driver");
                // 2 获得连接
                String url = "jdbc:mysql://rm-bp......nm1ao.mysql.rds.aliyuncs.com:3306/yth";
                conn = DriverManager.getConnection(url, "xiaoy1995", "Xiaoy9502");
                // 3 获取sql语句
                String sql = "select * from user where name = ?";
                // 4 获取预处理 statement
                stmt = conn.prepareStatement(sql);
                // 5 设置参数,序号从1开始
                stmt.setString(1, "刘备");
                // 6 执行SQL语句
                rs = stmt.executeQuery();
                // 7 处理结果集
                while (rs.next()) {
                    // 获得一行数据
                    System.out.println(rs.getString("name") + ", " + rs.getString("gender") + "," + rs.getString("phone")
                        + "," + rs.getString("address"));
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 8释放资源
                if (rs != null) {
                    try {
                        rs.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (stmt != null) {
                    try {
                        stmt.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (conn != null) {
                    try {
                        conn.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    

    使用JDBC方式编程,缺点很多就不说了。我们直接思考可以从哪些方面进行优化。

    1. 首先里面的数据源配置信息和sql信息是经常变化的,可以放到配置文件中
      在这里插入图片描述
    2. 每次JDBC执行SQL的步骤都是相似的,可以对各步骤所代表的功能进行分析,划分不同模块,抽象出通用的组件。

    针对上面两点,我们可以把JDBC按照下面两个流程进行分析

    • 解析流程
    • 执行流程
    解析流程(从配置文件中获取JDBC需要的数据信息)

    1.加载全局配置文件,最终将解析出来的信息,封装到【Configuration】对象中

    • a)解析运行时环境信息,这边主要指的是DataSource的配置信息,然后将DataSource封装到Configuration对象中存储。
    • b)当加载到mappers元素的时候,就会触发映射文件的加载。
      • 映射文件的加载,先解析select/update/insert/delete标签,获取id值(statementId)、SQL语句、参数类型、结果类型、statement类型,最终将解析出来的信息,封装到【MappedStatement】对象中,再将该对象封装到Map集合中,key为statementId,value就是该对象,然后将map集合存储到【Configuration】对象中
        • MappedStatement对象中存储Sql信息,是通过【SqlSource】进行存储的。SqlSource对象,不只是存储Sql信息,而且还提供对存储的SQL信息进行处理的功能。
        • SqlSource是通过一个SqlNode集合数据来封装的SQL信息。
      • 解析映射文件其他元素…
    执行流程(使用JDBC代码从配置文件中读取SQL相关信息完成CRUD)

    1.获取连接(需要driverclassname、url、username、password)

    • a)获取Configuration对象,从该对象中获取DataSource对象
    • b)有了DataSource对象,就可以从该对象中获取Connection对象

    对应的JDBC执行步骤:

     // 1 注册驱动
    Class.forName("com.mysql.jdbc.Driver");
    // 2 获得连接
    String url = "jdbc:mysql://rm-bp186l387tovm7nm1ao.mysql.rds.aliyuncs.com:3306/yth";
    conn = DriverManager.getConnection(url, "xiaoy1995", "Xiaoy9502");
    

    2.获取JDBC可以执行的SQL语句

    • 其实此处获取的是BoundSql,就是调用的【SqlSource】的SQL解析功能得到的
    • 从BoundSql中获取SQL语句

    对应的JDBC执行步骤:

    // 3 获取sql语句
    String sql = "select * from user where name = ?";
    

    3.获取statement对象

    • 从MappedStatement对象中获取statement的类型:simple、prepared、callable根据不同类型去创建不同的Statement对象

    对应的JDBC执行步骤:

    // 4 获取预处理 statement
    stmt = conn.prepareStatement(sql);
    

    4.从BoundSql中获取参数映射信息List<ParameterMapping>

    • 遍历给参数赋值,先需要读取ParameterMapping中的属性名称,再从入参对象中获取指定属性的值
    • 调用JDBC代码,完成属性设置

    对应的JDBC执行步骤:

    // 5 设置参数,序号从1开始
    stmt.setString(1, "刘备");
    

    5.执行statemement,并获取结果集ResultSet
    对应的JDBC执行步骤:

    // 6 执行SQL语句
    rs = stmt.executeQuery();
    

    6.处理结果集

    • 从MappedStatement对象中获取输出结果类型,也就是结果要映射的类型
    • 遍历结果集,获取结果集中每一行的数据
    • 获取结果集中每一行每一列的数据,获取列的名称
    • 根据列的名称,通过反射,去设置要映射的java类型的指定属性值

    对应的JDBC执行步骤:

    // 7 处理结果集
    while (rs.next()) {
    	// 获得一行数据
    	System.out.println(rs.getString("name") + ", " +.....);
    }
    
    展开全文
  • nodejs电商后台小项目

    千次阅读 2018-11-28 15:49:41
    REST API设计 ...当一个Web应用以API的方式对外提供功能和接口时,整个应用的架构模式是这样的: 但是,URL怎么约定,参数以什么编码方式传,响应数据的格式是什么样的,以及响应码怎么约定,API...

    REST API的设计

    前言

    客户端通过请求URL,传递参数,去获取指定的数据,这就是API(Application Programming Interface)。

    API是前端和客户端操作后端数据的一种方式,一种接口。当一个Web应用以API的方式对外提供功能和接口时,整个应用的架构模式是这样的:

    但是,URL怎么约定,参数以什么编码方式传,响应数据的格式是什么样的,以及响应码怎么约定,API版本升级怎么设计,这些问题是设计这些API的后端人员不得不考虑的问题。

    2000年,Roy Fielding博士在论文中提出REST(Representational State Transfer)风格的软件API架构模式。该模式对上面的问题提出了针对性的解决方案,迅速被大家接受,逐渐流行起来。

    REST是一种API的设计模式,它将数据库的数据看做是一个个的资源。客户端的请求就是对这些资源的操作,这些操作无外乎增删改查四种。这种模式简单易读,易于维护。

    这种设计模式提倡我们遵循以下原则。

    数据格式

    REST提倡数据格式应该使用轻量级,易于阅读的json格式。这要求我们除了GET请求方式外,其他请求方式的请求体都应该是json,响应体也是json。
    所以,请求头和响应头的Content-Type都应该为application/json

    请求方式

    REST将请求看做是对某个资源的增删改查操作,正好对应了Http请求的GET,POST, PUT, DELETE 4种请求方法。
    对应关系如下:

    - 获取资源:`GET`方式
    - 创建资源:`POST`方式
    - 更新资源:`PUT方`式
    - 删除资源:`DELETE`方式
    

    URL设计

    REST提倡声明式的URL设计。URL设计需要考虑层次化,可扩展,易于维护。每一个URL都表示对某个资源的操作,假设我们要操作服务器的博客数据,可以这样设计:

    • 通用的资源操作方式:

      // 获取所有博客
      GET /api/blogs  
      // 获取指定博客
      GET /api/blogs/1234
      // 创建博客
      POST /api/blogs
      // 更新指定博客
      PUT /api/blogs/1234
      // 删除指定博客
      DELETE /api/blogs/1234
      
    • 资源的过滤操作,使用查询参数来限定条件:

      分页获取部分博客
    GET /api/blogs?page=1&size=15&sort=time  
     
    - 按层次组织资源:
     
      // 获取某个博客下面的所有评论
      	
    	GET /api/blogs/143/comments
     
      层次化的设计有可能会让URL太长,不便于阅读,比如:
     
      // 获取某个博客下面的某个评论的某个回复
      
    	GET /api/blogs/123/comments/12343/replys/2231
     
      所以,我们也可以使用扁平化的方式设计所有的资源:
     
      // 获取某个博客的所有评论,使用查询参数来限定条件
      
    	GET /api/comments?blogId=123
      // 获取某个评论的所有回复
      
    	GET /api/replys?commentId=123
    
    • API的版本升级。
      我们的API会不断的更新迭代,比如:获取所有订单的URL参数多加了一个时间戳。如果我们直接更改原来接口的逻辑,会导致旧版本的客户端获取失败。因此,通过添加一个路径/v1来让我们的API更加维护性。

          // 获取所有订单, 第一版
          GET /api/v1/orders
          // 获取所有订单, 第二版
          GET /api/v2/orders
      

    响应设计

    响应设计分为数据结构设计和响应码设计。

    • 响应数据结构设计

      客户端对资源的操作有可能成功,也有可能失败。以前我一直使用使用这样的数据结构:

          {
              code: 1/0, // 会有很多标识码,11, 12, 13 ...
              msg: "获取成功/获取失败",
              data: [...]/{}
          }
      

      整体的结构一定不能变,数据的变化体现在data字段。这样非常方便客户端的解析或者序列化(静态语言)。

    • 响应码设计

      响应码分为http响应码和逻辑响应码。http响应码很简单,如果成功就200,服务器出错就500。而逻辑响应码则是需要自己来定义,比如:1表示用户不存在,2表示密码错误。

    前后端分离

    在以前的时代,用户请求一个网页,服务端通过JSP技术,或者其他模板引擎技术动态渲染后,返回给用户。它们看起来像这样:

    这样做的坏处是,后端和前端融合在一起,哪怕前端代码不是由后端人员来写,仍然需要2方人员进行必要的交流和沟通,降低了开发效率;而且前端和后端共同部署,灵活性差;而且后台处理静态资源的性能较差,也不应该去处理静态资源压缩,缓存等问题,应该交给代理服务器来处理,比如Nginx。

    前后端分离最极致作法是:前端和后端各自独立编写,独立部署,通过API接口进行交互。他们看起来像这样:

    项目实战:电商管理后台

    目标,实现一个有账户模块,权限模块,商品和分类模块和订单模块,给商家和管理员使用的后台系统。

    项目采用前后端分离的开发方式,本身只实现API功能,并没有提供界面。

    项目结构搭建和实施流程

    在之前TODO项目的基础上,增加一个middleware包和test包,前者用来存放中间件的包,因为权限管理需要用中间件来实现;后者是测试相关包。

    1. 每个模块的实现顺序为:model层 --> service层 --> router层。
    2. 单元测试:service层写脚本测试;router层使用postman测试。

    配置文件的环境切换

    开发环境和生产环境的配置一般是不一样的,比如端口配置,数据库配置。一般我们是通过环境变量NODE_ENV来区分。为了能够动态切换配置,就需要根据当前NODE_ENV的值来加载不同的配置对象。

    做法就是:

    1. 建立config目录,创建dev.jsprod.js分别存放对应的配置信息
    2. 编写index.js,实现动态切换配置的逻辑。

    目录结构:

    编写入口文件

    添加依赖:

    npm i body-parser express express-async-errors mongoose morgan
    

    编写app.jsdb.js文件。

    app.js

       //引入dib
       require('./db')
       
       const config = require('./config');
       const morgan = require('morgan')
       const bodyParser = require('body-parser');
       const express = require('express')
       // 引入异常捕获处理
       require('express-async-errors');
       
       const app = express();
       
       //注册中间件
       // log中间件
       app.use(morgan('combined'));
       
       //注册自定义的中间件
       
       // 注册body-parser中间件
       // parse application/x-www-form-urlencoded
       app.use(bodyParser.urlencoded({ extended: false }));
       // parse application/json
       app.use(bodyParser.json());
       
       // 注册路由
       app.use("/users", require('./router/account'));
       
       // 注册异常处理中间件
       app.use((err, req, res, next)=>{
           res.fail(err.toString())
       });
       
       //启动
       app.listen(config.PORT);
    
    
    

    db.js

    	const config = require('./config');
    	const mongoose  = require('mongoose');
    	mongoose.connect(`mongodb://127.0.0.1/${config.DB}`);
    	
    	const db = mongoose.connection;
    	
    	db.on('error', (err)=>{
    	    console.log(err);
    	});
    	
    	db.on("open", ()=>{
    	    console.log("mongodb connect successfully!");
    	});
     
    
    
    

    账户模块

    先编写model,再service,最后router;最后对service和router进行测试。

    REST中间件

    为了方便进行REST结果的返回,我们编写一个res_md.js中间件,作用是给每个res对象安装2个方法,注意该中间件注册的顺序尽量放在前面。代码如下:

       module.exports = function (req, res, next) {
         res.success = (data = null) =>{
           res.send({
               code: 0,
               msg: "操作成功",
               data: data
           })
         };
         res.fail = (msg)=>{
           res.send({
               code: -1,
               msg: msg
           })
         };
       
         next();
       };
    

    账户model

    	const mongoose = require('mongoose')
    	const schema = new mongoose.Schema({
    	    username: {
    	        type: String,
    	        required: [true, "用户名不能缺少"]
    	    },
    	    password: {
    	        type: String,
    	        required: [true, "密码不能缺少"]
    	    },
    	    age: {
    	        type: Number,
    	        min: [0, "年龄不能小于0"],
    	        max: [120, "年龄不能大于120"]
    	    },
    	    role: {
    	        type: Number,
    	        default: 0 // 0是商家, 10086是管理员
    	    },
    	    created:{
    	        type: Date,
    	        default: Date.now()
    	    }
    	});
    	
    	module.exports = mongoose.model('user', schema);
    
    

    账户service

    	const User = require('../model/user');
    	const config = require('../config')
    	const crypto = require('lxj-crypto')
    	
    	/**
    	 * 根据用户名获取某个用户
    	 * @param username
    	 * @returns {Promise<*>}
    	 */
    	async function getUserByUsername(username) {
    	    return await User.findOne({username: username}).select("-__v")
    	}
    	
    	
    	async function checkCanInsertUser(user) {
    	    //检查密码是否为空
    	    if(!user.password || user.password.length===0){ 
    	        throw Error("密码不能为空")
    	    }
    	    //检查用户是否已经存在
    	    let res = await getUserByUsername(user.username);
    	    if(res){
    	        throw Error("用户名已经存在")
    	    }
    	}
    	
    
    	/**
    	 * 添加普通用户
    	 * @param user
    	 * @returns {Promise<void>}
    	 */
    	async function addUser(user) {
    	    await checkCanInsertUser(user);
    	
    	    user.role = 0;
    	    user.created = Date.now();
    	
    	    //对密码进行加密,加密的方式:使用username作为随机数对password进行哈希
    	    user.password = crypto.md5Hmac(user.password, user.username)
    	    await User.create(user)
    	}
    	
    	async function deleteUser(id) {
    	    let res = await User.deleteOne({_id:id});
    	    if(!res || res.n===0){
    	        throw Error("删除用户失败")
    	    }
    	}
    	
    	/**
    	 * 登录的方法
    	 * @param user
    	 * @returns token
    	 */
    	async function login(user) {
    	    // 1. 对密码进行加密
    	    user.password = crypto.md5Hmac(user.password, user.username)
    	    // 2. 进行查询
    	    let res = await User.findOne({username:user.username, password: user.password});
    	    if(!res){
    	        throw Error("用户名或者密码错误")
    	    }
    	
    	    // 说明用户名和密码验证成功,需要生产token返回给客户端,以后客户端的header中带上这个token
    	    // token 生产方法:用aes进行对指定的data加密
    	    let tokenData = {
    	        username: user.username,
    	        expire: Date.now() + config.TokenDuration
    	    };
    	    let token = crypto.aesEncrypt(JSON.stringify(tokenData), config.TokenKey);
    	    return token
    	}
    	
    	module.exports = {
    	    getUserByUsername,
    	    addUser, 
    	    deleteUser,
    	    login
    	};
    	 
    
    

    账户router

    	let router = require("express").Router();
    	let accountService = require('../service/accout')
    	
    	
    	router.get("/:username", async (req, res)=>{
    	    let user = await accountService.getUserByUsername(req.params.username);
    	    res.success(user);
    	});
    	
    	// 登录
    	router.post("/login", async (req, res)=>{
    	    let token = await accountService.login(req.body);
    	    res.success({token});
    	});
    	
    	// 注册
    	router.post("/register", async (req, res)=>{
    	    await accountService.register(req.body)
    	    res.success()
    	});
    	
    	router.delete("/:id", async (req, res)=>{
    	    await accountService.deleteUser(req.params.id)
    	    res.success()
    	});
    	
    	module.exports = router;
    	 
    	
    	```
    
    
    
    展开全文
  • NodeJs04

    2018-08-07 12:05:00
    REST API设计 ...当一个Web应用以API的方式对外提供功能和接口时,整个应用的架构模式是这样的: 但是,URL怎么约定,参数以什么编码方式传,响应数据的格式是什么样的,以及响应码怎么约定,API版...
  • [平台API]修复了api接口界面附件上传报错的问题 [平台API]修复了x_program_center load模块api脚本出错的问题 [流程平台]修复了可调度人员权限判断错误的问题 [流程平台]修复了在特定情况下编号脚本执行错误的问题 ...
  • 是真实应用服务的抽象,每一个服务后面都有很多对应的容器来支持,通过Proxy的port服务selector决定服务请求传递给后端提供服务的容器,对外表现为一个单一访问接口,外部不需要了解后端如何运行,这给扩展或维护...

空空如也

空空如也

1 2
收藏数 33
精华内容 13
关键字:

对外接口和api怎么设计