相比Javascript,果然还是Java好用的多。
随着JavaScript的发展,目前的JS已经支持Class等多种新的关键字,当然,也就多了很多新的坑。
今天发现某个项目中,突然出现了很多JWT异常,观察了一下,JWT不知道为啥变成了null,不知道是那个前辈搞的,如果在发出请求之前,可以先对token进行判断,就不会由于这种问题导致后端大量报错甚至狗带了。
但是由于一些原因,发送http请求的方法很多,比如说getXXXX,然后postXXXX这样的,一个一个写显然很麻烦,而且会有大量代码重复,我就想到了Spring对这种情况的处理方式,就是AOP,通过CGLib可以对Java的Class进行代理,并且拦截相关方法,织入横切逻辑。
但是在js中,找了找没有类似的类库,这就比较麻烦了。
AOP是什么
AOP,指面向切面编程,是另外一种编程思路,对面向对象是一种补充,在面向对象编程中,有些时候会遇到这种问题:既,一些功能相同的逻辑会包围另外一些逻辑。
或者这样说,我希望在某些方法被调用的开始或者执行的结束,或者是这个方法的抛出了异常的时候进行一些额外的动作,那么有几种做法呢?
首先就是直接将代码放到方法中,这样的话,这些本来不属于这个方法的功能的代码就会扰乱方法本身的结构,让整体不方便后续的阅读,而且如果在很多的方法都需要这样的处理,也会导致代码的重复。当然,也可以采用继承的手法,不过如果这些方法是属于多种Class,继承也就不是很合适了。
AOP会在方法开始或者结束或者跑出异常等特定的场景下被触发和执行,这样原本包围在代码前后的无关逻辑就会被抽出,从而实现复用和简化。
那么这是怎么做到的呢?
代理。代理是用另外一个代理对象
代替本来的对象,在Java中,一般会在原始类的基础上生成一个新的类,这个新的类就包含了需要的,原本被抽取的逻辑部分,这一部分被抽取的逻辑也称为横切逻辑
,而实际上在使用经过了AOP的对象,其实使用的就是这种重新生成的,含有横切逻辑的代理类的对象。
像是这种运用代理方式对代码进行横向简化和复用的思路,就是AOP。
JavaScript实现AOP的基本思路
首先,要得到一个Object的所有Method(连接点,可以被其他逻辑包围的代码部分,一般是方法),然后根据某种规则进行匹配(切点,我们要替换掉,使用横切逻辑的代码部分),然后生成另一个Function(切面,包含了横切逻辑),让这个Function代替原有Function(织入,把横切代码和原本的方法合并的过程),并且在这里面执行横切逻辑。
为了达到这个目的,就要知道怎么得到一个JavaScript中Object的所有Method,最开始我是这样想的:
class Http {
getNews(type){
// weex原生的ajax
}
getUser(id){
// weex原生的ajax
}
// 省略部分方法
}
let instance = new Http();
// 遍历这个instance
for(let item in instance){
console.log(item);
}
我认为他应该就能够打印出所有的Method的名字,然而,啥也没有,这就比较郁闷了。
语法 | 适用范围 | 备注 |
---|---|---|
for(let elem in obj) | 可迭代对象,例如数组,可以得到key(键) | ES6的Class的实例是得不到里面的方法的 |
for(let elem of obj) | 数组之类的,可以得到内容(value) |
后来发现ES6的对象,自身有一个字段叫做__proto__
这个字段是不可以迭代的,在这里面就有Class的各种method,可以通过Object.getOwnPropertyNames
这个方法得到方法名称的数组。
既然是方法名称,想来拿他匹配切点应该没问题,因此我使用正则表达式作为切点的条件,只要方法名字可以被正则匹配到,那么就对他进行代理。
那么剩下的问题就是该怎么使用这个被拦截到的方法了。
Function的调用方法
方法 | 参数 | 含义 | 备注 |
---|---|---|---|
call | object,argument… | this代指的对象,(多个)函数参数,需要几个直接往后面加就行,他是可变的 | 参数是多个的,需要多少都可以写进去,第一个是函数内部的this |
apply | object,arguments | this代指的对象,函数参数数组 | 参数是一个数组,第一个也是函数内部的this |
Function的调用方法有apply和call,就是在javascript中实现AOP的关键。
apply方法接收两个参数,第一个是this,在Function内的this指的就是这个参数的对象,另一个是arguments,这是一个数组,包含了这个Function接收到的全部参数。
call方法接收不限制个数的参数,第一个也是Function内的this,剩下的就是function的参数,需要什么直接填就行。
前置增强和后置增强
export class Aspect {
/**
* 对某个对象添加前置增强
* @param object 代理目标
* @param regexp 切入点,使用正则表达式,按照方法名进行切入
* @param func 待织入的增强
*/
static beforeAdvice(object,regexp,func){
let names = [];
let isES6 = .isES6Instance(object);
// 获取方法列表
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
for (let item of names){
// 查找切入点,使用正则表达式匹配名称
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
let target =isES6?object.__proto__[item]:object[item];
// 生成代理方法,织入切面
let proxyed = function () {
try {
// 调用前置增强,传入当期方法名称以及参数
func.call(object,item,arguments);
// 调用原始方法并返回
return target.apply(object,arguments);
}catch (e) {
}
};
// 替换原有方法,完成横切
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
/**
* 为目标对象添加后置增强
* @param object 代理目标
* @param regexp 切入点
* @param func 待织入增强
*/
static afterAdvice(object,regexp,func){
// 获取方法名称列表
let names = [];
let isES6 = .isES6Instance(object);
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
for (let item of names){
// 查找切入点,使用正则表达式匹配方法名
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
let target =isES6?object.__proto__[item]:object[item];
// 生成代理方法,织入切面
let proxyed = function () {
try {
// 调用原始方法,保留返回值
let result = target.apply(object,arguments);
// 调用后置增强
func.call(object,item,arguments);
// 返回原方法的结果
return result;
}catch (e) {
}
};
// 替换原始方法,完成横切
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
static isES6Instance(obj){
if (typeof obj.__proto__ !== 'undefined' && obj.__proto__ != null){
return true
}else{
return false;
}
}
}
其实应该还有其他几个AOP增强,不过其实原理都差不多。
环绕增强
不同于之前的几个增强,环绕增强需要传递一个执行对象给切面,让切面决定什么时候执行原始方法,因此需要一个新的class对这个进行描述。
/**
* 切入点
* 用于封装原始方法,提供一个process方法用于执行原始方法。
* 原始方法的返回值既process的返回值
*/
class ExcPoint {
constructor(target,scope,param){
this.target = target;
this.scope = scope;
this.param = param;
}
process(){
return this.target.apply(this.scope,this.param);
}
}
然后,在Aspect的class中增加环绕增强的编织方法:
static asyncAroundAdvice(object,regexp,func){
// 获取方法名称列表
let names = [];
let isES6 = Aspect.isES6Instance(object);
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
for (let item of names){
// 寻找切入点,通过正则表达式对方法名称进行匹配
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
let target = isES6 ? object.__proto__[item] : object[item];
// 生成代理方法,编织切面
let proxyed = function () {
try {
// 封装原始方法
let point = new ExcPoint(target,object,arguments);
// 返回切面的处理结果
return func.call(object,item,point);
}catch (e) {
}
};
// 替换原有方法,完成横切
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
其实到了这里可以发现除了返回不太一样,执行的时机不太一样,其他部分是完全可以进行整合的,这一部分整合之后代码将会更加简洁。
完整的AOP工具类
export class Aspect {
/**
* 对某个对象添加前置增强
* @param object 代理目标
* @param regexp 切入点,使用正则表达式,按照方法名进行切入
* @param func 待织入的增强
*/
static beforeAdvice(object,regexp,func){
Aspect.aspectAware(object,regexp,func,0)
}
/**
* 为目标对象添加后置增强
* @param object 代理目标
* @param regexp 切入点
* @param func 待织入增强
*/
static afterAdvice(object,regexp,func){
Aspect.aspectAware(object,regexp,func,1)
}
/**
* 为目标对象添加环绕增强
* @param object 代理目标
* @param regexp 切入点
* @param func 待织入的增强
*/
static aroundAdvice(object,regexp,func){
Aspect.aspectAware(object,regexp,func,2)
}
/**
* 为目标添加异常时增强
* @param object 代理目标
* @param refexp 切入点
* @param func 待织入增强
*/
static afterThrowingAdvice(object,refexp,func){
Aspect.aspectAware(object,refexp,func,3)
}
/**
* 为目标对象添加增强
* @param object 目标对象
* @param regexp 切入点
* @param func 待织入增强
* @param type 0:前置 1:后置 2:环绕 3:异常
*/
static aspectAware(object,regexp,func,type){
let names = [];
let isES6 = Aspect.isES6Instance(object);
// 获取方法列表
if(isES6){
names = Object.getOwnPropertyNames(object.__proto__);
}else {
names = Object.keys(object);
}
// 寻找切入点
for (let item of names){
if(regexp.test(item) && (typeof object.__proto__[item] === 'function'
|| typeof object[item] === 'function')){
// 被代理的目标方法
let target = isES6 ? object.__proto__[item] : object[item];
let proxyed = null;
// 根据选择类型的不同生成不同的代理方法
if(type === 0){
// 前置增强
proxyed = function () {
func.call(object,item,arguments);
return target.apply(object,arguments);
};
}else if(type === 1){
// 后置增强
proxyed = function () {
let result = target.apply(object,arguments);
func.call(object,item,arguments);
return result;
};
}else if(type === 2){
// 环绕增强
proxyed = function () {
let point = new ExcPoint(target,object,arguments);
return func.call(object,item,point);
};
}else if(type === 3){
// 异常时增强
proxyed = function () {
try {
return target.apply(object,arguments);
}catch (e) {
return func.call(object,item,arguments);
}
}
}
// 替换原有方法,完整横切织入
if(isES6){
object.__proto__[item] = proxyed;
}else{
object[item] = proxyed;
}
}
}
}
/**
* 判别对象是否为ES6下的实例
* @param obj
* @returns {boolean}
*/
static isES6Instance(obj){
if (typeof obj.__proto__ !== 'undefined' && obj.__proto__ != null){
return true
}else{
return false;
}
}
}
class ExcPoint {
constructor(target,scope,param){
this.target = target;
this.scope = scope;
this.param = param;
}
process(){
return this.target.apply(this.scope,this.param);
}
}