Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
AST – Groovy Transformers:
More than meets the eye!
IVÁN LÓPEZ
@ilopmar
Hello!
I am Iván López
@ilopmar
@madridgug http://greachconf.com
“
The best code is not code at all
1.
A little bit of theory
AST and compilation
▷ Abstract Syntax Tree
▷ AST modified during compilation
▷ Hook into the compiler phases
AST Transformations
▷ Global ▷ Local
2.
Out-of-the-box ASTs
AST transformations categories
▷ Code generation
▷ Class design
▷ Logging improvements
▷ Declarative concurrency
▷ Cloning...
Code generation
@ToString
▷ Human readable toString
▷ Effective Java by Joshua Bloch (item 10)
class User {
String name
Integer age
}
def u = new User(name: 'Iván', age: 35)
println u // User@1d2a54b2
@groovy.transform.ToString
class User {
String name
Integer age
}
def u = new User(name: 'Iván', age: 35)
assert u.toStrin...
@groovy.transform.ToString
class User {
String name
Integer age
}
def u = new User(name: 'Iván', age: 35)
assert u.toStrin...
@ToString
▷ includeNames, excludes, includes, includeSuper,
includeSuperProperties, includeFields, ignoreNulls,
includePac...
@ToString
▷ includeNames, excludes, includes, includeSuper,
includeSuperProperties, includeFields, ignoreNulls,
includePac...
@EqualsAndHashCode
▷ Generate equals and hashCode implementations
▷ Effective Java items 8 & 9
@EqualsAndHashCode
▷ Generate equals and hashCode implementations
▷ Effective Java items 8 & 9
@groovy.transform.EqualsAnd...
int hashCode() {
def _result = HashCodeHelper.initHash()
_result = HashCodeHelper.updateHash(_result, this.name)
_result =...
@EqualsAndHashCode
▷ excludes, includes, callSuper, includeFields, cache,
useCanEqual
@EqualsAndHashCode
▷ excludes, includes, callSuper, includeFields, cache,
useCanEqual
@groovy.transform.EqualsAndHashCode(...
@TupleConstructor
▷ Generate constructors
@TupleConstructor
▷ Generate constructors
@groovy.transform.TupleConstructor
class User {
String name
Integer age
}
@TupleConstructor
▷ Generate constructors
@groovy.transform.TupleConstructor
class User {
String name
Integer age
}
// Def...
@TupleConstructor
▷ Generate constructors
@groovy.transform.TupleConstructor
class User {
String name
Integer age
}
// Def...
@TupleConstructor
▷ Generate constructors
@groovy.transform.TupleConstructor
class User {
String name
Integer age
}
// Def...
@TupleConstructor
▷ excludes, includes, includeFields, includeProperties,
includeSuperFields, includeSuperProperties,
call...
@Canonical
▷ @ToString + @EqualsAndHashCode +
@TupleConstructor
@Canonical
▷ @ToString + @EqualsAndHashCode +
@TupleConstructor
@groovy.transform.Canonical
class User {
String name
Integ...
@Canonical
▷ @ToString + @EqualsAndHashCode +
@TupleConstructor
def u1 = new User(name: 'Iván', age: 35)
assert u1.toStrin...
@Canonical
▷ @ToString + @EqualsAndHashCode +
@TupleConstructor
def u2 = new User('Iván', 35) // @TupleConstructor
assert ...
@Canonical
▷ @ToString + @EqualsAndHashCode +
@TupleConstructor
assert u1 == u2 // @EqualsAndHashCode
assert u1.hashCode()...
@InheritConstructors
▷ Reduce boilerplate code when parent classes
have multiple constructors
▷ Useful when overriding exc...
@groovy.transform.InheritConstructors
class MyException extends Exception {
}
protected MyException(String param0, Throwable param1, boolean param2, boolean param3) {
super(param0, param1, param2, par...
@Lazy
▷ Lazy initialization of fields
▷ Useful when creating expensive resources
▷ Effective Java item 71
class SomeBean {
@Lazy
LinkedList myField
}
class SomeBean {
@Lazy
LinkedList myField
}
public LinkedList getMyField() {
if ($myField != null) {
$myField
} else {
$my...
@Sortable
▷ Comparable interface
▷ compareTo method natural order
▷ N methods returning comparators
▷ Effective Java item ...
@groovy.transform.Sortable
class User {
String name
Integer age
Integer born
}
public int compareTo(User other) {
if (this.is(other)) return 0
Integer value = 0
value = this.name <=> other.name
if (val...
@groovy.transform.Sortable
class User {
String name
Integer age
Integer born
}
def users = [
new User(name: 'Mary', age: 15, born: 2000),
new User(name: 'Peter', age: 44, born: 1970),
new User(name: 'J...
assert users.sort(false, User.comparatorByName())*.name == ['John', 'Mary', 'Peter']
assert users.sort(false, User.compara...
@Sortable
▷ includes, excludes
@groovy.transform.Sortable(excludes = 'age')
class User {
String name
Integer age
Integer b...
@Builder
▷ Create fluent API calls
▷ Multiple building strategies
▷ Multiple configuration options: builder name,
prefix, ...
@groovy.transform.builder.Builder
class User {
String name
Integer age
Integer born
}
@groovy.transform.builder.Builder
class User {
String name
Integer age
Integer born
}
def u = User.builder()
.name('Iván')...
public static class User$UserBuilder extends Object {
private String name
private Integer age
private Integer born
public ...
Class design
@Delegate
▷ Implements delegation design pattern
▷ Delegate calls on object to method on delegated
properties
▷ All public...
import java.time.LocalDate
class Conference {
@groovy.lang.Delegate
LocalDate when
String name
}
import java.time.LocalDate
class Conference {
@groovy.lang.Delegate
LocalDate when
String name
}
def greach = new Conferen...
def greach = new Conference(name: 'Greach', when: LocalDate.of(2015, 04, 10))
def gr8conf = new Conference(name: 'GR8Conf'...
class Conference {
...
public boolean isAfter(ChronoLocalDate param0) {
when.isAfter(param0)
}
public boolean isBefore(Chr...
@Immutable
▷ Create immutable classes
▷ Effective Java item 15
▷ Rules for immutability
@groovy.transform.Immutable
class User {
String name
Integer age
}
def u = new User(name: 'Iván', age: 35)
// This does not compile
// You are not allowed to overwrite
// the final class 'User'.
class Admin extends User {
}
@groo...
@groovy.transform.Immutable
class User {
String name
Integer age
}
def u = new User(name: 'Iván', age: 35)
try {
u.name = ...
@Memoized
▷ Cache the result of a method
@Memoized
▷ Cache the result of a method
@groovy.transform.Memoized
Long fibonacci(Integer n) {
if (n < 2) return 1
else r...
@Memoized
▷ Cache the result of a method
@groovy.transform.Memoized
Long fibonacci(Integer n) {
if (n < 2) return 1
else r...
Logging improvements
@Log, @Log4j, @Log4j2, @Slf4j
▷ Static final field for the logger
@Log, @Log4j, @Log4j2, @Slf4j
@groovy.util.logging.Log4j
class MyClass {
void method() {
log.debug "My debug message"
}
}
...
Declarative concurrency
Declarative concurrency
▷ @Synchronized
▷ @WithReadLock
▷ @WithWriteLock
Cloning and externalizing
Cloning and externalizing
▷ @AutoClone
▷ @AutoExternalize
Safe scripting
Safe scripting
▷ @ThreadInterrupt
▷ @TimedInterrupt
▷ @ConditionalInterrupt
Compiler directives
Compiler directives
▷ @TypeChecked
▷ @CompileStatic
▷ @CompileDynamic
Dependencies handling
@Grab
▷ Grape dependency manager
@Grab
▷ Grape dependency manager
@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
import o...
@GrabResolver
▷ Grape dependency manager
@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
...
@GrabExclude
▷ Grape dependency manager
@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE')
i...
3.
Summary
“
The best code is not code at all
Thanks!
Any questions?
@ilopmar
lopez.ivan@gmail.com
https://github.com/lmivan
Iván López
http://kcy.me/1zr7q
Greach 2015   AST – Groovy Transformers: More than meets the eye!
Upcoming SlideShare
Loading in …5
×

Greach 2015 AST – Groovy Transformers: More than meets the eye!

1,549 views

Published on

Slides for my Greach 2015 talk: http://greachconf.com/speakers/ivan-lopez-ast-groovy-transformers-more-than-meets-the-eye/

The source code is: https://github.com/lmivan/greach2015

Groovy is a great language with extremely powerful capabilities about compile time meta-programming. Do you know that provides more than 40 AST transformations out-of-the box just to make your life as a developer easier?

In this talk you will learn the most important transformations provided by Groovy. I’ll use a lot of code examples to explain all the concepts.

Published in: Technology

Greach 2015 AST – Groovy Transformers: More than meets the eye!

  1. 1. AST – Groovy Transformers: More than meets the eye! IVÁN LÓPEZ @ilopmar
  2. 2. Hello! I am Iván López @ilopmar @madridgug http://greachconf.com
  3. 3. “ The best code is not code at all
  4. 4. 1. A little bit of theory
  5. 5. AST and compilation ▷ Abstract Syntax Tree ▷ AST modified during compilation ▷ Hook into the compiler phases
  6. 6. AST Transformations ▷ Global ▷ Local
  7. 7. 2. Out-of-the-box ASTs
  8. 8. AST transformations categories ▷ Code generation ▷ Class design ▷ Logging improvements ▷ Declarative concurrency ▷ Cloning and externalizing ▷ Safe scripting ▷ Compiler directives ▷ Dependencies handling
  9. 9. Code generation
  10. 10. @ToString ▷ Human readable toString ▷ Effective Java by Joshua Bloch (item 10)
  11. 11. class User { String name Integer age } def u = new User(name: 'Iván', age: 35) println u // User@1d2a54b2
  12. 12. @groovy.transform.ToString class User { String name Integer age } def u = new User(name: 'Iván', age: 35) assert u.toString() == 'User(Iván, 35)' class User { String name Integer age } def u = new User(name: 'Iván', age: 35) println u // User@1d2a54b2
  13. 13. @groovy.transform.ToString class User { String name Integer age } def u = new User(name: 'Iván', age: 35) assert u.toString() == 'User(Iván, 35)' String toString() { def _result = new StringBuilder() _result.append('User(') _result.append(this.name) _result.append(', ') _result.append(this.age) _result.append(')') return _result.toString() } class User { String name Integer age } def u = new User(name: 'Iván', age: 35) println u // User@1d2a54b2
  14. 14. @ToString ▷ includeNames, excludes, includes, includeSuper, includeSuperProperties, includeFields, ignoreNulls, includePackage, cache
  15. 15. @ToString ▷ includeNames, excludes, includes, includeSuper, includeSuperProperties, includeFields, ignoreNulls, includePackage, cache @groovy.transform.ToString(includeNames = true, excludes = ['name']) class User { String name Integer age } def u = new User(name: 'Iván', age: 35) assert u.toString() == 'User(age:35)'
  16. 16. @EqualsAndHashCode ▷ Generate equals and hashCode implementations ▷ Effective Java items 8 & 9
  17. 17. @EqualsAndHashCode ▷ Generate equals and hashCode implementations ▷ Effective Java items 8 & 9 @groovy.transform.EqualsAndHashCode class User { String name Integer age } def u1 = new User(name: 'Iván', age: 35) def u2 = new User(name: 'Iván', age: 35) assert u1 == u2 assert u1.hashCode() == u2.hashCode()
  18. 18. int hashCode() { def _result = HashCodeHelper.initHash() _result = HashCodeHelper.updateHash(_result, this.name) _result = HashCodeHelper.updateHash(_result, this.age) return _result } boolean canEqual(Object other) { return other instanceof User } boolean equals(Object other) { if (other == null) { return false } if (this.is(other)) { return true } if (!(other instanceof User)) { return false } User otherTyped = ((other) as User) if (!(otherTyped.canEqual(this))) { return false } if (!(this.getName().is(otherTyped.getName()))) { if (this.getName().is(this) && !(otherTyped.getName().is(otherTyped)) || !(this.getName().is(this)) && otherTyped.getName().is(otherTyped)) return false } else { if (!(this.getName().is(this) && otherTyped.getName().is(otherTyped))) { if (!(this.getName() == otherTyped.getName())) { return false } } } } if (!(this.getAge().is(otherTyped.getAge()))) { if (this.getAge().is(this) && !(otherTyped.getAge().is(otherTyped)) || !(this.getAge().is(this)) && otherTyped.getAge().is(otherTyped)) { return false } else { if (!(this.getAge().is(this) && otherTyped.getAge().is(otherTyped))) { if (!(this.getAge() == otherTyped.getAge())) { return false } } } } return true }
  19. 19. @EqualsAndHashCode ▷ excludes, includes, callSuper, includeFields, cache, useCanEqual
  20. 20. @EqualsAndHashCode ▷ excludes, includes, callSuper, includeFields, cache, useCanEqual @groovy.transform.EqualsAndHashCode(includes = 'name') class User { String name Integer age } def u1 = new User(name: 'Iván', age: 35) def u2 = new User(name: 'Iván', age: 42) assert u1 == u2 assert u1.hashCode() == u2.hashCode()
  21. 21. @TupleConstructor ▷ Generate constructors
  22. 22. @TupleConstructor ▷ Generate constructors @groovy.transform.TupleConstructor class User { String name Integer age }
  23. 23. @TupleConstructor ▷ Generate constructors @groovy.transform.TupleConstructor class User { String name Integer age } // Default map constructor def u1 = new User(name: 'Iván', age: 35)
  24. 24. @TupleConstructor ▷ Generate constructors @groovy.transform.TupleConstructor class User { String name Integer age } // Default map constructor def u1 = new User(name: 'Iván', age: 35) // Generated tuple constructor def u2 = new User('Iván', 35) def u3 = new User('Iván')
  25. 25. @TupleConstructor ▷ Generate constructors @groovy.transform.TupleConstructor class User { String name Integer age } // Default map constructor def u1 = new User(name: 'Iván', age: 35) // Generated tuple constructor def u2 = new User('Iván', 35) def u3 = new User('Iván') User(String name = null, Integer age = null) { this.name = name this.age = age }
  26. 26. @TupleConstructor ▷ excludes, includes, includeFields, includeProperties, includeSuperFields, includeSuperProperties, callSuper, force
  27. 27. @Canonical ▷ @ToString + @EqualsAndHashCode + @TupleConstructor
  28. 28. @Canonical ▷ @ToString + @EqualsAndHashCode + @TupleConstructor @groovy.transform.Canonical class User { String name Integer age }
  29. 29. @Canonical ▷ @ToString + @EqualsAndHashCode + @TupleConstructor def u1 = new User(name: 'Iván', age: 35) assert u1.toString() == 'User(Iván, 35)' // @ToString @groovy.transform.Canonical class User { String name Integer age }
  30. 30. @Canonical ▷ @ToString + @EqualsAndHashCode + @TupleConstructor def u2 = new User('Iván', 35) // @TupleConstructor assert u2.toString() == 'User(Iván, 35)' def u1 = new User(name: 'Iván', age: 35) assert u1.toString() == 'User(Iván, 35)' // @ToString @groovy.transform.Canonical class User { String name Integer age }
  31. 31. @Canonical ▷ @ToString + @EqualsAndHashCode + @TupleConstructor assert u1 == u2 // @EqualsAndHashCode assert u1.hashCode() == u2.hashCode() // @EqualsAndHashCode def u2 = new User('Iván', 35) // @TupleConstructor assert u2.toString() == 'User(Iván, 35)' def u1 = new User(name: 'Iván', age: 35) assert u1.toString() == 'User(Iván, 35)' // @ToString @groovy.transform.Canonical class User { String name Integer age }
  32. 32. @InheritConstructors ▷ Reduce boilerplate code when parent classes have multiple constructors ▷ Useful when overriding exception classes
  33. 33. @groovy.transform.InheritConstructors class MyException extends Exception { }
  34. 34. protected MyException(String param0, Throwable param1, boolean param2, boolean param3) { super(param0, param1, param2, param3) } public MyException(Throwable param0) { super(param0) } public MyException(String param0, Throwable param1) { super(param0, param1) } public MyException(String param0) { super(param0) } public MyException() { super() } @groovy.transform.InheritConstructors class MyException extends Exception { }
  35. 35. @Lazy ▷ Lazy initialization of fields ▷ Useful when creating expensive resources ▷ Effective Java item 71
  36. 36. class SomeBean { @Lazy LinkedList myField }
  37. 37. class SomeBean { @Lazy LinkedList myField } public LinkedList getMyField() { if ($myField != null) { $myField } else { $myField = new LinkedList() } }
  38. 38. @Sortable ▷ Comparable interface ▷ compareTo method natural order ▷ N methods returning comparators ▷ Effective Java item 12
  39. 39. @groovy.transform.Sortable class User { String name Integer age Integer born }
  40. 40. public int compareTo(User other) { if (this.is(other)) return 0 Integer value = 0 value = this.name <=> other.name if (value != 0) return value value = this.age <=> other.age if (value != 0) return value value = this.born <=> other.born if (value != 0) return value return 0 } private static class User$NameComparator extends AbstractComparator<User> { public int compare(User arg0, User arg1) { if (arg0 == arg1) return 0 if (arg0 != null && arg1 == null) return -1 if (arg0 == null && arg1 != null) return 1 return arg0.name <=> arg1.name } } private static class User$AgeComparator extends AbstractComparator<User> { ... } @groovy.transform.Sortable class User { String name Integer age Integer born }
  41. 41. @groovy.transform.Sortable class User { String name Integer age Integer born }
  42. 42. def users = [ new User(name: 'Mary', age: 15, born: 2000), new User(name: 'Peter', age: 44, born: 1970), new User(name: 'John', age: 35, born: 1979), ] @groovy.transform.Sortable class User { String name Integer age Integer born }
  43. 43. assert users.sort(false, User.comparatorByName())*.name == ['John', 'Mary', 'Peter'] assert users.sort(false, User.comparatorByAge())*.born == [2000, 1979, 1970] def users = [ new User(name: 'Mary', age: 15, born: 2000), new User(name: 'Peter', age: 44, born: 1970), new User(name: 'John', age: 35, born: 1979), ] @groovy.transform.Sortable class User { String name Integer age Integer born }
  44. 44. @Sortable ▷ includes, excludes @groovy.transform.Sortable(excludes = 'age') class User { String name Integer age Integer born } def users = [ new User(name: 'Mary', age: 15, born: 2000), new User(name: 'Peter', age: 44, born: 1970), new User(name: 'John', age: 35, born: 1979), ] assert users.sort(false, User.comparatorByName())*.name == ['John', 'Mary', 'Peter'] assert users.sort(false, User.comparatorByAge())*.born == [2000, 1979, 1970]
  45. 45. @Builder ▷ Create fluent API calls ▷ Multiple building strategies ▷ Multiple configuration options: builder name, prefix, excludes, includes,... ▷ Effective Java item 2
  46. 46. @groovy.transform.builder.Builder class User { String name Integer age Integer born }
  47. 47. @groovy.transform.builder.Builder class User { String name Integer age Integer born } def u = User.builder() .name('Iván') .age(35) .born(1979) .build() assert u.name == 'Iván' assert u.age == 35 assert u.born == 1979
  48. 48. public static class User$UserBuilder extends Object { private String name private Integer age private Integer born public User$UserBuilder() { } public User$UserBuilder name(String name) { this.name = name return this } public User$UserBuilder age(Integer age) { this.age = age return this } public User$UserBuilder born(Integer born) { this.born = born return this } public User build() { User _theUser = new User() _theUser.name = name _theUser.age = age _theUser.born = born return _theUser } } @groovy.transform.builder.Builder class User { String name Integer age Integer born } def u = User.builder() .name('Iván') .age(35) .born(1979) .build() assert u.name == 'Iván' assert u.age == 35 assert u.born == 1979
  49. 49. Class design
  50. 50. @Delegate ▷ Implements delegation design pattern ▷ Delegate calls on object to method on delegated properties ▷ All public methods are delegated
  51. 51. import java.time.LocalDate class Conference { @groovy.lang.Delegate LocalDate when String name }
  52. 52. import java.time.LocalDate class Conference { @groovy.lang.Delegate LocalDate when String name } def greach = new Conference(name: 'Greach', when: LocalDate.of(2015, 04, 10)) def gr8conf = new Conference(name: 'GR8Conf' when: LocalDate.of(2015, 06, 02))
  53. 53. def greach = new Conference(name: 'Greach', when: LocalDate.of(2015, 04, 10)) def gr8conf = new Conference(name: 'GR8Conf' when: LocalDate.of(2015, 06, 02)) assert greach.isBefore(gr8conf) import java.time.LocalDate class Conference { @groovy.lang.Delegate LocalDate when String name }
  54. 54. class Conference { ... public boolean isAfter(ChronoLocalDate param0) { when.isAfter(param0) } public boolean isBefore(ChronoLocalDate param0) { when.isBefore(param0) } ... } def greach = new Conference(name: 'Greach', when: LocalDate.of(2015, 04, 10)) def gr8conf = new Conference(name: 'GR8Conf' when: LocalDate.of(2015, 06, 02)) assert greach.isBefore(gr8conf) import java.time.LocalDate class Conference { @groovy.lang.Delegate LocalDate when String name }
  55. 55. @Immutable ▷ Create immutable classes ▷ Effective Java item 15 ▷ Rules for immutability
  56. 56. @groovy.transform.Immutable class User { String name Integer age } def u = new User(name: 'Iván', age: 35)
  57. 57. // This does not compile // You are not allowed to overwrite // the final class 'User'. class Admin extends User { } @groovy.transform.Immutable class User { String name Integer age } def u = new User(name: 'Iván', age: 35)
  58. 58. @groovy.transform.Immutable class User { String name Integer age } def u = new User(name: 'Iván', age: 35) try { u.name = 'John' } catch (ReadOnlyPropertyException e) { println e } // This does not compile // You are not allowed to overwrite // the final class 'User'. class Admin extends User { }
  59. 59. @Memoized ▷ Cache the result of a method
  60. 60. @Memoized ▷ Cache the result of a method @groovy.transform.Memoized Long fibonacci(Integer n) { if (n < 2) return 1 else return fibonacci(n-1) + fibonacci(n-2) } fibonacci(300)
  61. 61. @Memoized ▷ Cache the result of a method @groovy.transform.Memoized Long fibonacci(Integer n) { if (n < 2) return 1 else return fibonacci(n-1) + fibonacci(n-2) } fibonacci(300) @groovy.transform.Memoized User getUserInfo(Long userId) { // Expensive repetitive // network operation }
  62. 62. Logging improvements
  63. 63. @Log, @Log4j, @Log4j2, @Slf4j ▷ Static final field for the logger
  64. 64. @Log, @Log4j, @Log4j2, @Slf4j @groovy.util.logging.Log4j class MyClass { void method() { log.debug "My debug message" } } ▷ Static final field for the logger
  65. 65. Declarative concurrency
  66. 66. Declarative concurrency ▷ @Synchronized ▷ @WithReadLock ▷ @WithWriteLock
  67. 67. Cloning and externalizing
  68. 68. Cloning and externalizing ▷ @AutoClone ▷ @AutoExternalize
  69. 69. Safe scripting
  70. 70. Safe scripting ▷ @ThreadInterrupt ▷ @TimedInterrupt ▷ @ConditionalInterrupt
  71. 71. Compiler directives
  72. 72. Compiler directives ▷ @TypeChecked ▷ @CompileStatic ▷ @CompileDynamic
  73. 73. Dependencies handling
  74. 74. @Grab ▷ Grape dependency manager
  75. 75. @Grab ▷ Grape dependency manager @Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate // or @Grab('org.springframework:spring-orm:3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate
  76. 76. @GrabResolver ▷ Grape dependency manager @Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate // or @Grab('org.springframework:spring-orm:3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate @GrabResolver(name='restlet', root='http://maven.restlet.org/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6')
  77. 77. @GrabExclude ▷ Grape dependency manager @Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate // or @Grab('org.springframework:spring-orm:3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate @GrabResolver(name='restlet', root='http://maven.restlet.org/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') @Grab('net.sourceforge.htmlunit:htmlunit:2.8') @GrabExclude('xml-apis:xml-apis')
  78. 78. 3. Summary
  79. 79. “ The best code is not code at all
  80. 80. Thanks! Any questions? @ilopmar lopez.ivan@gmail.com https://github.com/lmivan Iván López http://kcy.me/1zr7q

×