2014. május 4., vasárnap

Spring Beans + EL = Magic! :-)

Néhány napja, mikor egy melóban felmerülő feladatra kerestem megoldásokat, belefutottam a Spring hasznos cuccaiba.

Elsőként a BeanWrapper-re találtam rá, mely arra lesz jó, hogy automatizáltan és flexibilisebben tudok feltölteni objektumokat értékekkel, akár egy properties fájl alapján. A lényege az, hogy String-ben mondhatom meg, hogy a becsomagolt objektum melyik adattagjával akarok operálni, ő pedig a háttérben lerendezi nekem, nyilván Reflection-el. Ez nagyon szépen működik addig, amíg a property útvonal létezik. Például "cég.cím.város" esetén a "cím" mezőben lennie kell egy megfelelő objektumnak, hogy a "város" adattagját be lehessen állítani.


Szerencsére ez is megoldható a Spring eszközeivel! Van egy Spring Expression Language nevű cucc, mellyel hívhatok konstruktort is, és ennek segítségével fel tudom építeni a property útvonalat teljes egészében. Így:
private void setDeepProperty(Object rootObject, String property,
    Object value) {

  // this will help accessing fields by name:
  BeanWrapper wrapper = new BeanWrapperImpl(rootObject);

  // this will help calling a constructor:
  ExpressionParser parser = new SpelExpressionParser();

  // go thru property path elements:
  int offset = 0;
  while ((offset = property.indexOf(".", offset + 1)) > -1) {
    String currentProperty = property.substring(0, offset);

    // if current property is null:
    if (null == wrapper.getPropertyValue(currentProperty)) {

      // get type of property:
      String className = wrapper.getPropertyType(currentProperty)
              .getName();

      // build up a constructor call:
      Expression exp = parser.parseExpression(String.format(
          "new %s()", className));

      // LIMITATIONS:
      // 1) uses static type
      // 2) needs defined empty constructor

      // run it:
      Object newObject = exp.getValue();

      // and set the property:
      wrapper.setPropertyValue(currentProperty, newObject);
    }
  }

  // finally, set the destination property:
  wrapper.setPropertyValue(property, value);
}
Ez tehát a fenti "cég.cím.város" példánál, ha a gyökér mondjuk maga a cég, a hivatkozott property pedig a "cím.város", akkor szépen létre fogja hozni a "cím"-et is. Hosszabb property útvonalnál pedig minden útba eső, még nem létező mezőnek értéket ad.

Ahogy a kódba is beleírtam kommentként, vannak azért ennek korlátai:

  1. Egyrészt a konstruktort a statikus típusra használja, tehát ha valamelyik adattag típusa egy absztrakt osztály, vagy interfész (pl. List), máris borul a dolog. Exception lesz a vége.
  2. Másrészt - bár ez némileg kapcsolódik az előzőhöz - léteznie kell az üres konstruktor definíciónak, hogy ez működjön. Még akkor is, ha nincs más konstruktor.
Ha ezekkel meg tudunk barátkozni, egész hasznos lehet a fenti függvény szerintem. :-)

A hosszú kód miatt inkább elé írom: ami még megfigyelés, hogy azért időigényes ez a dolog, a fenti függvény lefuttatása átlagosan 0.22 sec volt az alábbi kis példákban, ami baromi sok. 3 mérés átlagai:

  • wrap: ~54ms
  • getPropertyValue: ~30ms
  • getPropertyType: ~37ms
  • parseExpression: ~30ms
  • parser.getValue (konstruktorhívás): ~70ms
  • setPropertyValue: ~5ms
  • overall: ~226ms
EDIT: Ugyanakkor az is megfigyelhető, hogy a függvény későbbi hívásaikor már néhány ms alatt lefut az egész, tehát nem olyan rossz a helyzet. :)

Néhány JUnit teszt a bemutatására:
private static class Level0 {
  private int id;
  private Level1 lev1;

  public Level0() { }
  public int getId() { return id; }
  public void setId(int id) { this.id = id; }
  public Level1 getLev1() { return lev1; }
  public void setLev1(Level1 lev1) { this.lev1 = lev1; }
}

private static class Level1 {
  private int id;
  private Level2 lev2;

  public Level1() { }
  public int getId() { return id; }
  public void setId(int id) { this.id = id; }
  public Level2 getLev2() { return lev2; }
  public void setLev2(Level2 lev2) { this.lev2 = lev2; }
}

private static class Level2 {
  private int id;

  public Level2() { }
  public int getId() { return id; }
  public void setId(int id) { this.id = id; }
}

@Test
public void lev1IsNull() {
  assertNull(new Level0().getLev1());
}

@Test
public void buildPathToPrimitive() {
  Level0 lev0 = new Level0();
  setDeepProperty(lev0, "lev1.lev2.id", 1);
  assertEquals(1, lev0.getLev1().getLev2().getId());
}

@Test
public void buildPathToBean() {
  Level0 lev0 = new Level0();
  Level2 lev2 = new Level2();
  setDeepProperty(lev0, "lev1.lev2", lev2);
  assertEquals(lev2, lev0.getLev1().getLev2());
}

@Test
public void skipNonNulls() {
  Level0 lev0 = new Level0();
  Level1 lev1 = new Level1();
  lev1.setId(3);
  lev0.setLev1(lev1);
  assertEquals(3, lev0.getLev1().getId());
  setDeepProperty(lev0, "lev1.lev2.id", 6);
  assertEquals(3, lev0.getLev1().getId());
  assertEquals(6, lev0.getLev1().getLev2().getId());
}

Nincsenek megjegyzések: