awakeBird Back-end Dev Engineer

Python Web 基础向(五) api层


提供HTTP服务的api层负责处理以下任务:

  • 管理路由
  • 获取并检查参数
  • 将请求分发给相应的逻辑层 (简单的函数调用,略)
  • 包装返回数据
  • 处理错误

api层的代码应简单清晰,甚至你的每个路由对应的函数都“长得”差不多,这部分代码不应掺杂任何逻辑处理,他要做的仅仅是将请求指向相应的逻辑类去处理。

管理路由

Flask较为简单,一般来说,要实现基本功能,只需在api目录下生成蓝图和视图函数,在app.py中注册蓝图,并添加404和500的处理即可。

from my_project.api.student import student_api

logger = logging.getLogger(__name__)

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(error):
    logger.exception('api not found: {}'.format(error))
    return 'This api is not exits.', 404


@app.errorhandler(500)
def server_error(error):
    logger.exception('server error: {}'.format(error))
    return 'Server error.', 500

app.register_blueprint(student_api, url_prefix='/student')

除此之外, 需要考虑与前端交互的跨域请求访问情况,这部分可依赖CORS实现。

cors = CORS(app, resources={r"/student/*": {"origins": "*"},}, supports_credentials=True)

当然,你还需要测试你的server是否可用的ping路由和默认路由。

@app.route('/ping')
@cross_origin()
def ping():
    return True

@app.route('/')
def hello_world():
    return u'Hello World!'

更进一步,可以借助钩子在请求前后完成一些额外的工作,例如判断前端(或app)的设备类型,记录一层请求的用时等,也可在这部分完成。

@app.before_request
def before_request():
    stat_ctx = dict()
    stat_ctx['method'] = request.method
    stat_ctx['url'] = request.url
    stat_ctx['start_time'] = time.time()
    request.stat_ctx = stat_ctx


@app.after_request
def after_request(response):
    stat_ctx = request.stat_ctx
    stat_ctx['end_time'] = time.time()
    logger.info(u'[stat_ctx] my_project: {method} {url} response in {time}'.format(method=stat_ctx['method'], url=stat_ctx['url'], time=stat_ctx['end_time'] - stat_ctx['start_time']))
    return response

至此,app.py的功能基本完整,其他涉及具体业务的api可以在my_project/api目录下新开蓝图。

获取并检查参数

HTTP协议在请求中添加参数有多种方式,相应的,flask的Request对象会提供多种获取参数的方式。在获取参数时需要弄清楚参数传递的具体类型,否则不能正确拿到参数。

  • URL参数 将参数放在url中,可通过request.args.get('key')获取。
  • Form-data 通过HTML的form标签传递参数,通过request.form.get('key')获取。
  • x-www-form-urlencoded 与From-data类似,只是请求方式有区别。获取参数方式相同。
  • application/json 最常见的参数传递方式,通过request.json.get('key')获取。

flask获取请求参数的方法有的基于werkzuegBaseRequest对象实现,有的自己做了封装,使用时注意参照源码。这里为了方便,可以在工具类中加入参数的部分。

# util/params.py

import json

def get_params(request):
    """获取request中的参数"""
    params = dict()
    if request.values:
        for k in request.values:
            params[k] = request.values.get(k)
    if request.get_data():
        params.update(json.loads(request.get_data()))
    return params

获取列表数据也可以构建相应方法,或在代码中直接获取,这里不再赘述。 有一点需要注意,get_data方法获取参数时从stream中读取数据,而request对象中很多方法在调用之后会清空stream,导致无法通过get_data拿到数据,尽量将其作为兜底逻辑而不是首选。

组合并包装响应

这部分只需要构造相应的工具类中的responsetrans函数即可,需要注意的是关键路径上的查询接口。

降级策略

一般而言,服务器需要将查询接口的结果完整地返回给前端,但服务器也有开小差的情况,为了防止比较重要的查询接口因为一些小的或者非关键路径上的服务的错误而全部挂掉,或者在高并发场景下需要牺牲一部分内容或者功能的显示来换取负载上的优化,需要对这类接口建立降级策略。

降级根据介入方式可以分为主动降级和自动降级。

主动降级简而言之就是可以通过手动开关来限制对某个接口或服务的调用,从而实现功能降级,实现方式也是通过在接口前设置降级开关来实现。

而自动降级则与服务熔断的某些特征相似,当某个接口或服务异常被捕获或异常量超过阈值,则自动触发降级逻辑。

降级是微服务框架兴起后经常被提起的名词,目前有许多相对成熟的开源轮子如Hystrix完整地实现了服务降级配置功能,有兴趣可以自行查阅。这里通过一段简单代码说明降级的策略和作用(当然市面上的各种微服务框架的降级策略要复杂的多)

# api/student.py

@student_api.route('/info/update', methods=['GET'])
def update_student_info():

    rtn = dict()

    def _get_grade(student_id):
        student_info = StudentQuery().get_student_info_with_student_id(student_id)
        rtn[student_id]['grade'] = student_info.grade

    def _get_default_grade(student_id):
        rtn[student_id]['student_info'] = None

    tags = request.args.getlist("tags")
    params = get_params(request)
    student_id = params.get('student_id')

    if "grade" in tags:
        try:
            _get_grade(student_id)
        except Exception as e:
            logger.error("get student grade error: {}".format(e))
            _get_default_grade(student_id)
            
    return response(ResponseCode.SUCCEED, data=rtn)

处理错误

如果没有外部rpc调用,可以在util/response.py中增加捕捉错误的装饰器即可,这里需要将所有的错误包住,记录日志,并返回相应的错误码和信息。如果存在外部rpc调用,则可以在每个rpc目录下新增相应的错误捕获代码。

# util/response.py

from my_project.exc import (
    ProjectUserException,
    ProjectSystemException,
    ProjectUnknownException
)

def custom_exception_capture(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except (ProjectUserException, ProjectSystemException) as e:
            logger.error(e)
            return response(e.code, e.message or
                            ResponseCodeExplain[ResponseCode.SYSTEM_ERROR])
        except ProjectUnknownException as e:
            logger.error(e)
            return response(e.code, e.message or
                            ResponseCodeExplain[ResponseCode.SYSTEM_ERROR])
        except Exception as e:
            logger.exception(e)
            return response(ResponseCode.FAILED)
    return wrapper

(End)


Comments

Content