背景
最近有同事反應峰档,我們運營后臺下載的 CSV 文件出現(xiàn)錯亂的情況。問題的原因是原始數(shù)據(jù)中有 CSV 中非法的字符早像,比如說姓名字段,因為是用戶填寫的肖爵,內容有可能包含了 ,
卢鹦、"
等字符,會導致 CSV 文件內容錯亂劝堪。
于是我就想用一個簡單的方式來解決這個問題冀自。一個簡單粗暴的解決方案就是導出時對字符串進行處理,將一些特殊字符替換掉秒啦,或者前后用"
包起來熬粗。但是這樣的話,需要所有下載 CSV 的地方都要改寫余境,會比較麻煩驻呐。如果我們可以簡單的給 String 增加一個方法(如 String.csv()
)直接就把字符串處理成 CSV 兼容的格式,就會方便很多葛超。我們的運營后臺是使用 Scala 語言開發(fā)的暴氏,所幸的是,Scala 里提供了一個非常強大的功能绣张,可以滿足我們的需求答渔,那就是隱式轉換。
Scala 的隱式轉換
在 Scala 里可以通過 implicit
隱式轉換來實現(xiàn)函數(shù)擴展侥涵。
編譯器在碰到類型不匹配或是調用一個不存在的方法的時候沼撕,會去搜索符合條件的隱式類型轉換,如果找不到合適的隱式轉換方法則會報錯芜飘。
下面是處理 CSV 下載字符串的代碼:
trait CsvHelper {
implicit def stringToCsvString(s: String) = new CsvString(s)
}
class CsvString(val s: String){
def csv = s"""${s.replaceAll(",", " ").replaceAll("\"", "'")}"""
}
class Controller extends CsvHelper {
def dowload(){
...
",foo,".csv //foo
}
}
在 Controller
中我調用 String.csv
方法务豺,但是 String
沒有 csv
方法。這時候編譯器就會去找 Controller
中有沒有隱式轉換的方法嗦明,發(fā)現(xiàn)在其父類 CsvHelper
中有方法把 String
轉換成 CsvString
笼沥,而 CsvString
中實現(xiàn)了 csv
方法。所以編譯器最終會調用到 CsvString.csv
這個方法。
隱式轉換是一個很強大奔浅,但是也很容易誤用的功能馆纳。Scala 里隱式轉換有一些基本規(guī)則:
- 優(yōu)先規(guī)則:如果存在兩個或者多個符合條件的隱式轉換,如果編譯器不能選擇一條最優(yōu)的隱式轉換汹桦,則提示錯誤鲁驶。具體的規(guī)則是:當前類中的隱式轉換優(yōu)先級大于父類中的隱式轉換;多個隱式轉換返回的類型有父子關系的時候舞骆,子類優(yōu)先級大于父類钥弯。
- 隱式轉換只會隱式的調用一次,編譯器不會調用多個隱式方法督禽,不會產生調用鏈脆霎。
- 如果當期代碼已經是合法的,不需要隱式轉換則不會使用隱式轉換狈惫。
Java 的動態(tài)擴展
我們再來看看我們熟悉的 Java 語言绪穆。Java 是一門靜態(tài)語言,本身沒有直接提供動態(tài)擴展的方法虱岂,但是我們可以通過 AOP 動態(tài)代理的方式來修改一個方法玖院,從而間接的實現(xiàn)方法的動態(tài)擴展。
下面就是一個我們就用 AspectJ
來實現(xiàn)一個動態(tài)擴展第岖,用于分頁查詢后獲取數(shù)據(jù)的總條數(shù)难菌。
@Aspect
@Component
public class PaginationAspect {
@AfterReturning(
pointcut = "execution(* com.xingren..*.*ByPage(..))",
returning = "result"
)
public void afterByPage(JoinPoint joinPoint, Object result) {
//根據(jù)result獲取sql信息,再查詢總條數(shù)封裝到result中蔑滓。
}
}
其中 AfterReturning
注解表明在被注解方法返回后的一些后續(xù)動作郊酒。pointcut
定義切點的表達式,可以用通配符 *
表示键袱;returning
指定返回的參數(shù)名燎窘。然后就可以對返回的結果進行處理。這樣就可以達到動態(tài)的修改原始函數(shù)功能蹄咖。
當然除了 AspectJ
也可以使用 CGLib
來代理來實現(xiàn)簡單的 AOP褐健。
public class FooService {
public Page findByPage(){
return new Page();
}
public Page findPage(){
return new Page();
}
}
@Data
public class Page {
private String sql = "";
private List<Object> content = new ArrayList();
private Integer size = 0;
private Integer page = 0;
private Integer total = 0;
}
創(chuàng)建一個對象 FooService
用來模擬查詢分頁方法。
public class CGLibProxyFactory implements MethodInterceptor {
private Object object;
public CGLibProxyFactory(Object object){
this.object = object;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before method! do something...");
Object result = methodProxy.invoke(object, objects);
//進行方法判斷澜汤,是否需要處理
if (method.getName().contains("ByPage")) {
if (result instanceof Page) {
System.out.println("after method! do something...");
((Page) result).setTotal(100);
}
}
return result;
}
}
創(chuàng)建一個代理類實現(xiàn) MethodInterceptor
接口蚜迅,手動調用 invoke
方法,用來動態(tài)的修改被代理的實現(xiàn)方法俊抵∷唬可以在執(zhí)行之前做一些參數(shù)校驗,或者一些參數(shù)的預處理徽诲。也可以獲取修改執(zhí)行的結果刹帕,或者干脆不調用 invoke
方法吵血,自定義實現(xiàn)。也可以在調用后做一些后續(xù)動作偷溺。
public class ObjectFactoryUtils {
public static <T> Optional<T> getProxyObject(Class<T> clazz) {
try {
T obj = clazz.newInstance();
CGLibProxyFactory factory = new CGLibProxyFactory(obj);
Enhancer enhancer=new Enhancer();//利用`Enhancer`來創(chuàng)建被代理類的代理實例
enhancer.setSuperclass(clazz);//設置目標class
enhancer.setCallback(factory);//設置回調代理類
return Optional.of((T)enhancer.create());
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return Optional.empty();
}
}
public static void main(String[] args) {
Optional<FooService> proxyObject = ObjectFactoryUtils.getProxyObject(FooService.class);
if(proxyObject.isPresent()) {
FooService foo = proxyObject.get();
System.out.println("findByPage:");
System.out.println(foo.findByPage().getTotal());
System.out.println("findPage:");
System.out.println(foo.findPage().getTotal());
}
}
最后打印的輸出是:
findByPage:
before method! do something...
after method! do something...
100
findPage:
before method! do something...
0
當然除了 CGLIB 代理也可以使用 Proxy 動態(tài)代理践瓷,同樣的邏輯也可以達到動態(tài)的修改原始方法的目的,從而間接的實現(xiàn)函數(shù)擴展亡蓉。不過 Proxy 動態(tài)代理是基于接口的代理。
其它語言的函數(shù)擴展
其實除了 Scala 的隱式轉換和 Java 的動態(tài)代理喷舀,其他很多語言也能支持各種不同的函數(shù)擴展砍濒。
Swift
在 Swift 中可以通過關鍵詞 extension
對已有的類進行擴展,可以擴展方法硫麻、屬性爸邢、下標、構造器等等拿愧。
extension Int {
func times(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
比如說我給 Int 增加一個 times 方法杠河。即執(zhí)行任務的次數(shù)。就可以如下使用:
2.times({
print("Hello!")
})
上面的代碼會執(zhí)行 2 次打印方法浇辜。
Go
在 Go 中可以通過在方法名前面加上一個變量券敌,這個附加的參數(shù)會將該函數(shù)附加到這種類型上。即給一個方法加上接收器柳洋。
func (s string) toUpper() string {
return strings.ToUpper(s)
}
"aaaaa".toUpper //輸出 AAAAA
Kotlin
Kotlin 的函數(shù)擴展非常簡單待诅,就是定義的時候,函數(shù)名寫成 接收器
+ .
+ 方法名
就行了熊镣。
class C {
}
fun C.foo() { println("extension") }
C().foo() //輸出extension
注意當給一個類擴展已有的方法的時候卑雁,默認使用的是類自帶的成員函數(shù)。如下:
class C {
fun foo() { println("member") }
}
fun C.foo() { println("extension") }
C().foo() //輸出member
可以通過函數(shù)重載的方式區(qū)分成員函數(shù)(fun C.foo(i:Int) { println("extension") }
)绪囱,在調用的地方顯示的區(qū)分测蹲。
JavaScript
在 JavaScript 中也可以很方便的給一個對象擴展函數(shù)。寫法就是 對象
+ .
+ 函數(shù)名
鬼吵。
var date = new Date();
date.format = function() {
return this.toISOString().slice(0, 10);
}
date.format(); //"2017-11-29"
也可以給一個 Object 進行擴展:
Date.prototype.format = function() {
return this.toISOString().slice(0, 10);
}
new Date().format(); //"2017-11-29"
總結
其實了解不同語言對于函數(shù)擴展的實現(xiàn)挺有意思的扣甲,本文只是粗略的介紹了一下。合理的使用這些語言的擴展齿椅,可以幫助我們提高代碼質量和工作效率文捶。我們還可以通過函數(shù)擴展來對第三方類庫進行修改或者擴展,從而更靈活的調用第三方類庫媒咳。