一前言
面向切面编程 (AOP) 是一种编程思想, 与 OOP 并不矛盾, 只是它们的关注点相同面向对象的目的在于抽象和管理, 而面向切面的目的在于解耦和复用
举两个大家都接触过的 AOP 的例子:
1)java 中 mybatis 的 @Transactions 注解, 大家知道被这个注解注释的函数立即就能获得 DB 的事务能力
2)python 中的 with threading.Lock(), 大家知道, 被这个 with 代码块包裹的部分立即获得同步的锁机制
这样我们把事务和加锁这两种与业务无关的逻辑抽象出来, 在逻辑上解耦, 并且可以轻松的做到代码复用
二上下文管理器 contextlib
当然你可以使用 with 上下文管理器实现一些 AOP 的思想, 这里有个模块叫 contextlib 可以帮助你简易的实现上下文管理器
上下文管理最常见的例子是 with open('file') as fh, 回收打开句柄的例子
这种方式还是比较麻烦的, 下面我们看一下 python 中的装饰器怎么样实现 AOP 编程
三装饰器: AOP 的语法糖
python 中的装饰器就是设计来实现切面注入功能的下面给出几个例子, 这几个例子都是在生产环境验证过的
其中的任务管理机是伪代码, 需要自己实现写数据库的逻辑
1 重试逻辑
只要 do 函数被 @retry_exp 装饰, 便可以获得指数退避的重试能力
- @retry_exp(max_retries=10)
- def do():
- # do whatever
- pass
那 retry_exp 是如何实现的呢?
- def retry_exp(max_retries=3, max_wait_interval=10, period=1, rand=False):
- def _retry(func):
- def __retry(*args, **kwargs):
- MAX_RETRIES = max_retries
- MAX_WAIT_INTERVAL = max_wait_interval
- PERIOD = period
- RAND = rand
- retries = 0
- error = None
- ok = False
- while retries < MAX_RETRIES:
- try:
- ret = func(*args, **kwargs)
- ok = True
- return ret
- except Exception, ex:
- error = ex
- finally:
- if not ok:
- sleep_time = min(2 ** retries * PERIOD if not RAND else randint(0, 2 ** retries) * PERIOD, MAX_WAIT_INTERVAL)
- time.sleep(sleep_time)
- retries += 1
- if retries == MAX_RETRIES:
- if error:
- raise error
- else:
- raise Exception("unknown")
- return __retry
- return _retry
2 降级开关
只要 do 函数被 @degrade 装饰, 就会安装 app 名称校验 redis 里的开关, 一旦发现开关关闭, 则 do 函数不被执行, 也就是降级
- @degrade
- def do(app):
- # do whatever
- pass
那么 degrade 是怎样实现的呢?
- def degrade(app):
- def _wrapper(function):
- def __wrapper(*args, **kwargs):
- value = None
- try:
- redis = codis_pool.get_connection()
- value = redis.get("dmonitor:degrade:%s" % app)
- except Exception, _:
- logger.info(traceback.format_exc())
- if not value or int(value) != 1:
- function()
- logger.info("[degrade] is_on: %s" % app)
- else:
- logger.info("[degrade] is_off: %s" % app)
- return __wrapper
- return _wrapper
3 任务状态机
这个是最常用的, 我们需要跟踪落盘 DB 一个任务的执行状态(等待调度, 执行中, 执行成功, 执行失败)
一旦 do 方法被 @tasks_decorator 装饰, 就获得了这样的能力对 item_param(是个 json)中 task_id 指明的任务进行状态管理
- @tasks_decorator
- def do(item_param):
- # do whatever
- pass
tasks_decorator 是怎样实现的呢?
- def tasks_decorator(function):
- def _wrap(*args, **kwargs):
- param_dict = kwargs.get('item_param')
- task_id = param_dict.get('task_id')
- try:
- param_dict.update({'status': TaskStatus.Waiting, 'start_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
- try:
- manager_dao.save_task(param_dict)
- except Exception, ex:
- pass
- _update_task_status(task_id, TaskStatus.Doing)
- function(*args, **kwargs)
- _update_task_status(task_id, TaskStatus.Done)
- except Exception as e:
- time.sleep(0.5)
- _update_task_status(task_id, TaskStatus.Fail, unicode(e.message))
- raise
- return _wrap
4 全局唯一性
在分布式 + 异步环境中, 如果想保证 exactly once 是需要额外的逻辑的, 其实主要是实现唯一键, 一旦唯一键实现了, 就可以使用公共缓存 redis 进行唯一键判定了
do 函数被 unique 装饰, 那么对于 task_id 对应的任务, 全局只会执行一次
- @unique
- def do(task_id):
- # do whatever
- pass
unique 是怎样实现的呢?
- def unique(function):
- def _wrap(*args, **kwargs):
- task_id = kwargs.get('task_id')
- try:
- redis = codis_pool.get_connection()
- key = "unique:%s" % task_id
- if not redis.setnx(key):
- redis.expire(key, 24*60*60)
- function(*args, **kwargs)
- except Exception as e:
- logger.error(traceback.format_exc())
- raise
- return _wrap
四总结
AOP 在少量增加代码复杂度的前提下, 显著的获得以下优点:
1 使得功能逻辑和业务逻辑解耦, 功能和业务的修改完全独立, 代码结构清晰, 开发方便
2 一键注入, 代码复用程度高, 扩展方便
来源: https://www.cnblogs.com/kangoroo/p/8522942.html