Ognl 使用實例手冊

  • 2019 年 12 月 16 日
  • 筆記

上一篇博文介紹了ongl的基礎語法,接下來進入實際的使用篇,我們將結合一些實際的case,來演示ognl究竟可以支撐到什麼地步

在看本文之前,強烈建議先熟悉一下什麼是ognl,以及其語法特點,減少閱讀障礙,五分鐘入門系列: 191129-Ognl 語法基礎教程

<!– more –>

I. 基本使用

1. 配置

我們選用的是java開發環境,使用maven來進行包管理,首先在pom文件中添加依賴

<!-- https://mvnrepository.com/artifact/ognl/ognl -->  <dependency>      <groupId>ognl</groupId>      <artifactId>ognl</artifactId>      <version>3.2.11</version>  </dependency>

2. 基礎使用

對於Ognl的使用,關鍵的地方在於獲取OgnlContext, 在這個上下文中保存一些實例用來支撐ognl的語法

所以一般使用ognl的先前操作就是創建OgnlContext,然後將我們的實例扔到上下文中,接收ognl表達式,最後執行並獲取結果

偽程式碼如下

// 構建一個OgnlContext對象  OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this,          new DefaultMemberAccess(true),          new DefaultClassResolver(),          new DefaultTypeConverter());      // 設置根節點,以及初始化一些實例對象  context.setRoot(this);  context.put("實例名", obj);  ...      // ognl表達式執行  Object expression = Ognl.parseExpression("#a.name")  Object result = Ognl.getValue(expression, context, context.getRoot());

II. 實例演示

接下來進入實例演示,首先我們需要創建兩個測試對象,用於填充OgnlContext

0. 準備

兩個普通對象,一個靜態類

@Data  @NoArgsConstructor  @AllArgsConstructor  public class ADemo {        private String name;        private Integer age;    }    @Data  public class PrintDemo {        private String prefix;        private ADemo aDemo;        public void sayHello(String name, int age) {          System.out.println("name: " + name + " age: " + age);      }        private void print(ADemo a) {          System.out.println(prefix + " => " + a);      }        public <T> T print(String str, Class<T> clz) {          T obj = JSON.parseObject(str, clz);          System.out.println("class: " + obj);          return obj;      }        public void print(String str, String clz) {          System.out.println("str2a: " + str + " clz: " + clz);      }        public void print(String str, OgnlEnum ognlEnum) {          System.out.println("enum: " + str + ":" + ognlEnum);      }        public void print(String str, ADemo a) {          System.out.println("obj: " + str + ":" + a);      }        public void show(Class clz) {          System.out.println(clz.getName());      }  }    public class StaticDemo {        private static int num = (int) (Math.random() * 100);        public static int showDemo(int a) {          System.out.println("static show demo: " + a);          return a;      }  }    public enum OgnlEnum {      CONSOLE, FILE;  }

上面在創建OgnlContext時,有一個DefaultMemberAccess類,主要用於設置成員的訪問許可權,需要自己實現

@Setter  public class DefaultMemberAccess implements MemberAccess {      private boolean allowPrivateAccess = false;      private boolean allowProtectedAccess = false;      private boolean allowPackageProtectedAccess = false;        public DefaultMemberAccess(boolean allowAllAccess) {          this(allowAllAccess, allowAllAccess, allowAllAccess);      }        public DefaultMemberAccess(boolean allowPrivateAccess, boolean allowProtectedAccess,              boolean allowPackageProtectedAccess) {          super();          this.allowPrivateAccess = allowPrivateAccess;          this.allowProtectedAccess = allowProtectedAccess;          this.allowPackageProtectedAccess = allowPackageProtectedAccess;      }        @Override      public Object setup(Map context, Object target, Member member, String propertyName) {          Object result = null;            if (isAccessible(context, target, member, propertyName)) {              AccessibleObject accessible = (AccessibleObject) member;                if (!accessible.isAccessible()) {                  result = Boolean.TRUE;                  accessible.setAccessible(true);              }          }          return result;      }        @Override      public void restore(Map context, Object target, Member member, String propertyName, Object state) {          if (state != null) {              ((AccessibleObject) member).setAccessible((Boolean) state);          }      }        /**       * Returns true if the given member is accessible or can be made accessible by this object.       */      @Override      public boolean isAccessible(Map context, Object target, Member member, String propertyName) {          int modifiers = member.getModifiers();          if (Modifier.isPublic(modifiers)) {              return true;          } else if (Modifier.isPrivate(modifiers)) {              return this.allowPrivateAccess;          } else if (Modifier.isProtected(modifiers)) {              return this.allowProtectedAccess;          } else {              return this.allowPackageProtectedAccess;          }      }  }

接下來創建我們的OgnlContext對象

ADemo a = new ADemo();  a.setName("yihui");  a.setAge(10);    PrintDemo print = new PrintDemo();  print.setPrefix("ognl");  print.setADemo(a);      // 構建一個OgnlContext對象  // 擴展,支援傳入class類型的參數  OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this,                new DefaultMemberAccess(true), new DefaultClassResolver(), new DefaultTypeConverter());  context.setRoot(print);  context.put("print", print);  context.put("a", a);

到此,我們的前置準備已經就緒,接下來進入實際case篇

1. 實例訪問

我們的實例訪問分為兩類,分別為實例的方法調用;實例的屬性訪問

a. 實例方法調用

比如我們希望執行 print的sayHello方法,可以如下使用

Object ans = Ognl.getValue(Ognl.parseExpression("#print.sayHello("一灰灰blog", 18)"), context, context.getRoot());  System.out.println("實例方法執行: " + ans);

關鍵點在ognl表達式: #print.sayHello("一灰灰blog", 18),其中print為實例名,對應的構建OgnlContext對象之後執行的context.put("print", print);這一行程式碼

輸出結果:

name: 一灰灰blog age: 18  實例方法執行: null

b. 實例成員屬性訪問

成員屬性的訪問可以劃分為直徑獲取成員屬性值和設置成員屬性值,對此可以如下使用

ans = Ognl.getValue(Ognl.parseExpression("#a.name="一灰灰Blog""), context, context.getRoot());  System.out.println("實例屬性設置: " + ans);    ans = Ognl.getValue(Ognl.parseExpression("#a.name"), context, context.getRoot());  System.out.println("實例屬性訪問: " + ans);

輸出結果

實例屬性設置: 一灰灰Blog  實例屬性訪問: 一灰灰Blog

看到上面這個,自然會想到一個問題,可不可以訪問父類的私有成員呢?

為了驗證這個問題,我們新建一個實例繼承自ADemo,並註冊到 OgnlContext 上下文

@Data  public class BDemo extends ADemo {      private String address;  }    // 註冊到ognlContext  BDemo b = new BDemo();  b.setName("b name");  b.setAge(20);  b.setAddress("測試ing");  context.put("b", b);    // 測試case  ans = Ognl.getValue(Ognl.parseExpression("#b.name"), context, context.getRoot());  System.out.println("實例父類屬性訪問:" + ans);

輸出結果如下

實例父類屬性訪問:b name

注意:

我們這裡可以直接訪問私有成員,訪問私有方法,訪問父類的私有成員,這些都得益於我們自定義的DefaultMemberAccess,並制定了訪問策略為true(即私有、保護、默認訪問許可權的都可以訪問)

2. 靜態類訪問

實例成員,需要先註冊到OgnlContext之後才能根據實例名來訪問,但是靜態類則不需要如此,默認支援當前的ClassLoader載入的所有靜態類的訪問姿勢;下面我們進入實例演示

a. 靜態類方法調用

靜態類的訪問需要注意的是需要傳入全路徑,用@開頭,類與方法之間也是用@進行分割

ans = Ognl.getValue(Ognl.parseExpression("@git.hui.fix.test.ognl.bean.StaticDemo@showDemo(20)"), context,          context.getRoot());  System.out.println("靜態類方法執行:" + ans);

輸出結果

static show demo: 20

a. 靜態類成員訪問

同樣我們分為成員訪問和修改

ans = Ognl.getValue(Ognl.parseExpression("@git.hui.fix.test.ognl.bean.StaticDemo@num"), context,          context.getRoot());  System.out.println("靜態類成員訪問:" + ans);    ans = Ognl.getValue(Ognl.parseExpression("@git.hui.fix.test.ognl.bean.StaticDemo@num=1314"), context,          context.getRoot());  System.out.println("靜態類成員設置:" + ans);

輸出結果如下

靜態類方法執行:20    ognl.InappropriateExpressionException: Inappropriate OGNL expression: @git.hui.fix.test.ognl.bean.StaticDemo@num    	at ognl.SimpleNode.setValueBody(SimpleNode.java:312)  	at ognl.SimpleNode.evaluateSetValueBody(SimpleNode.java:220)  	at ognl.SimpleNode.setValue(SimpleNode.java:301)  	at ognl.ASTAssign.getValueBody(ASTAssign.java:53)  	at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)

直接設置靜態變數,拋出了移倉,提示InappropriateExpressionException

那麼靜態類的成員可以修改么?這裡先留一個疑問

3. 特殊傳參

一般的java操作,無外乎方法調用,屬性訪問兩種,接下來我們聚焦在方法的調用上;如果一個方法接收的參數是一些基本類型的對象,使用起來還比較簡單;但是其他的場景呢?

a. class類型參數

如我們前面的PrintDemo中,有一個方法如下

public <T> T print(String str, Class<T> clz) {      T obj = JSON.parseObject(str, clz);      System.out.println("class: " + obj);      return obj;  }

如需調用上面的方法,clz參數可以怎麼處理呢?

ans = Ognl.getValue(Ognl.parseExpression(          "#print.print("{'name':'xx', 'age': 20}", @git.hui.fix.test.ognl.bean.ADemo@class)"), context,          context.getRoot());  System.out.println("class 參數方法執行:" + ans);    // class傳參  ans = Ognl.getValue(Ognl.parseExpression("#print.print("{'name':'haha', 'age': 10}", #a.getClass())"),          context, context.getRoot());  System.out.println("class 參數方法執行:" + ans);

上面給出了兩種方式,一個是根據已有的對象獲取class,一個是直接根據靜態類獲取class,輸出結果如下

class: ADemo(name=xx, age=20)  class 參數方法執行:ADemo(name=xx, age=20)  class: ADemo(name=haha, age=10)  class 參數方法執行:ADemo(name=haha, age=10)

b. 枚舉參數

如PrintDemo中的方法, 其中第二個參數為枚舉

public void print(String str, OgnlEnum ognlEnum) {      System.out.println("enum: " + str + ":" + ognlEnum);  }

結合上面的使用姿勢,這個也不太難

ans = Ognl.getValue(          Ognl.parseExpression("#print.print("print enum", @git.hui.fix.test.ognl.model.OgnlEnum@CONSOLE)"),          context, context.getRoot());  System.out.println("枚舉參數方法執行:" + ans);

輸出結果

enum: print enum:CONSOLE  枚舉參數方法執行:null

c. null傳參

目標方法如下

private void print(ADemo a) {      System.out.println(prefix + " => " + a);  }

因為我們需要傳參為空對象,稍微有點特殊,ognl針對這個進行了支援,傳參直接填null即可

ans = Ognl.getValue(Ognl.parseExpression("#print.print(null)"), context, context.getRoot());  System.out.println("null 傳參:" + ans);

輸出如下

ognl => null  null 傳參:null

然後一個問題來了,在PrintDemo中的print方法,有多個重載的case,那麼兩個參數都傳null,具體是哪個方法會被執行呢?

public <T> T print(String str, Class<T> clz) {      T obj = JSON.parseObject(str, clz);      System.out.println("class: " + obj);      return obj;  }    public void print(String str, String clz) {      System.out.println("str2a: " + str + " clz: " + clz);  }    public void print(String str, OgnlEnum ognlEnum) {      System.out.println("enum: " + str + ":" + ognlEnum);  }    public void print(String str, ADemo a) {      System.out.println("obj: " + str + ":" + a);  }

通過實際的測試,第三個方法被調用了,這裡面難道有啥潛規則么,然而我並沒有找到

ans = Ognl.getValue(Ognl.parseExpression("#print.print(null, null)"), context, context.getRoot());  System.out.println("null 傳參:" + ans);

輸出

enum: null:null  null 傳參:null

d. 對象傳遞

傳參是一個POJO對象,這個時候咋整?

public void print(String str, ADemo a) {      System.out.println("obj: " + str + ":" + a);  }

現在的問題主要集中在如何構建一個Aemo對象,當做參數丟進去,通過前面的語法篇我們知道ognl是支援new來創建對象的, 如果ADemo恰好提供了全屬性的構造方法,那麼可以如下操作

ex = Ognl.parseExpression("#print.print("對象構建", new git.hui.fix.test.ognl.bean.ADemo("test", 20))");  Object ans = Ognl.getValue(ex, context, context.getRoot());  System.out.println("對象傳參:" + ans);

注意觀察上面的ognl表達式,其中重點在new git.hui.fix.test.ognl.bean.ADemo("test", 20)),創建對象的時候,請指定全路徑名

輸出結果

obj: 對象構建:ADemo(name=test, age=20)  對象傳參:null

上面這個雖然實現了我們的case,但是有局限性,如果這個POJO沒有全屬性的構造方法,又可以怎麼整?

這裡就需要藉助ognl語法中的鏈式語句了,通過new創建對象,然後設置屬性,最後拋出對象

ex = Ognl.parseExpression("#print.print("對象構建", (#demo=new git.hui.fix.test.ognl.bean.ADemo(), #demo.setName("一灰灰"), #demo))");  ans = Ognl.getValue(ex, context, context.getRoot());  System.out.println("對象傳參:" + ans);

核心語句在(#demo=new git.hui.fix.test.ognl.bean.ADemo(), #demo.setName("一灰灰"), #demo),創建對象,設置屬性

輸出結果

obj: 對象構建:ADemo(name=一灰灰, age=null)  對象傳參:null

雖說上面實現了我們的需求場景,但是這裡有個坑,我們創建的這個屬性會丟到OgnlContext上下文中,所以這種操作非常有可能導致我們自己創建的臨時對象覆蓋了原有的對象

那麼有什麼方法可以避免么?

這個問題先攢著,後面再敘說

e. 容器傳參

在PrintDemo對象中添加方法

public void print(List<Integer> args) {      System.out.println(args);  }    public void print(Map<String, Integer> args) {      System.out.println(args);  }

然後我們的訪問case如下

ex = Ognl.parseExpression("#print.print({1, 3, 5})");  ans = Ognl.getValue(ex, context, context.getRoot());  System.out.println("List傳參:" + ans);    ex = Ognl.parseExpression("#print.print(#{"A": 1, "b": 3, "c": 5})");  ans = Ognl.getValue(ex, context, context.getRoot());  System.out.println("Map傳參:" + ans);

輸出結果

[1, 3, 5]  List傳參:null  {A=1, b=3, c=5}  Map傳參:null

4. 表達式執行

接下來屬於另外一個範疇的case了,執行一些簡單的算術操作or條件表達式

ans = Ognl.getValue(Ognl.parseExpression("1 + 3 + 4"), context, context.getRoot());  System.out.println("表達式執行: " + ans);    // 階乘  ans = Ognl.getValue(Ognl.parseExpression("#fact = :[#this<=1? 1 : #this*#fact(#this-1)], #fact(3)"), context, context.getRoot());  System.out.println("lambda執行: " + ans);

輸出

表達式執行: 8  lambda執行: 6

III. 小結

鑒於篇幅過長,本篇博文將只限於使用基礎的ognl能支援到什麼地步,在java中使用ognl套路比較簡單

1. 創建OgnlContext,並註冊實例

// 構建一個OgnlContext對象  OgnlContext context = (OgnlContext) Ognl.createDefaultContext(this,          new DefaultMemberAccess(true),          new DefaultClassResolver(),          new DefaultTypeConverter());      // 設置根節點,以及初始化一些實例對象  context.setRoot(this);  context.put("實例名", obj);  ...

2. 編譯ognl表達式,並獲取執行結果

// ognl表達式執行  Object expression = Ognl.parseExpression("#a.name")  Object result = Ognl.getValue(expression, context, context.getRoot());

3. 遺留

博文中遺留了兩個問題尚未解答

  • 靜態成員默認場景下不能修改,那麼有辦法讓它支援修改么
  • 方法傳參,傳遞對象時,通過鏈式創建臨時對象時會快取在OgnlContext上下文中,如何避免這種場景?

II. 其他

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 聲明

盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激