Lombok 快速入门
Lombok 简介
Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 hashCode()
和 equals()
、getter / setter
这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。
Lombok 安装
由于 Lombok 仅在编译阶段生成代码,所以使用 Lombok 注解的源代码,在 IDE 中会被高亮显示错误,针对这个问题可以通过安装 IDE 对应的插件来解决。具体的安装方式可以参考:新版idea可略过
使 IntelliJ IDEA 支持 Lombok 方式如下:
- Intellij 设置支持注解处理
- 点击 File > Settings > Build > Annotation Processors
- 勾选 Enable annotation processing
- 安装插件
- 点击 Settings > Plugins > Browse repositories
- 查找 Lombok Plugin 并进行安装
- 重启 IntelliJ IDEA
- 将 lombok 添加到 pom 文件
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.8</version> </dependency>
Lombok 使用
Lombok 提供注解 API 来修饰指定的类:
@Getter and @Setter
@Getter and @Setter Lombok 代码:
@Getter @Setter private boolean employed = true; @Setter(AccessLevel.PROTECTED) private String name;
等价于 Java 源码:
private boolean employed = true; private String name; public boolean isEmployed() { return employed; } public void setEmployed(final boolean employed) { this.employed = employed; } protected void setName(final String name) { this.name = name; }
@NonNull
@NonNull Lombok 代码:
@Getter @Setter @NonNull private List<Person> members;
等价于 Java 源码:
@NonNull private List<Person> members; public Family(@NonNull final List<Person> members) { if (members == null) throw new java.lang.NullPointerException("members"); this.members = members; } @NonNull public List<Person> getMembers() { return members; } public void setMembers(@NonNull final List<Person> members) { if (members == null) throw new java.lang.NullPointerException("members"); this.members = members; }
@ToString
@ToString Lombok 代码:
@ToString(callSuper=true,exclude="someExcludedField") public class Foo extends Bar { private boolean someBoolean = true; private String someStringField; private float someExcludedField; }
等价于 Java 源码:
public class Foo extends Bar { private boolean someBoolean = true; private String someStringField; private float someExcludedField; @java.lang.Override public java.lang.String toString() { return "Foo(super=" + super.toString() + ", someBoolean=" + someBoolean + ", someStringField=" + someStringField + ")"; } }
@EqualsAndHashCode
@EqualsAndHashCode Lombok 代码:
@EqualsAndHashCode(callSuper=true,exclude={"address","city","state","zip"}) public class Person extends SentientBeing { enum Gender { Male, Female } @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; }
等价于 Java 源码:
public class Person extends SentientBeing { enum Gender { /*public static final*/ Male /* = new Gender() */, /*public static final*/ Female /* = new Gender() */; } @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; @java.lang.Override public boolean equals(final java.lang.Object o) { if (o == this) return true; if (o == null) return false; if (o.getClass() != this.getClass()) return false; if (!super.equals(o)) return false; final Person other = (Person)o; if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false; if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false; if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false; return true; } @java.lang.Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + super.hashCode(); result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode()); result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode()); return result; } }
@Data
@Data Lombok 代码:
@Data(staticConstructor="of") public class Company { private final Person founder; private String name; private List<Person> employees; }
@Data :注解在类上;提供类所有属性的 get
和 set
方法,此外还提供了equals
、canEqual
、hashCode
、toString
方法。
等价于 Java 源码:
public class Company { private final Person founder; private String name; private List<Person> employees; private Company(final Person founder) { this.founder = founder; } public static Company of(final Person founder) { return new Company(founder); } public Person getFounder() { return founder; } public String getName() { return name; } public void setName(final String name) { this.name = name; } public List<Person> getEmployees() { return employees; } public void setEmployees(final List<Person> employees) { this.employees = employees; } @java.lang.Override public boolean equals(final java.lang.Object o) { if (o == this) return true; if (o == null) return false; if (o.getClass() != this.getClass()) return false; final Company other = (Company)o; if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) return false; if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false; if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) return false; return true; } @java.lang.Override public int hashCode() { final int PRIME = 31; int result = 1; result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode()); result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode()); return result; } @java.lang.Override public java.lang.String toString() { return "Company(founder=" + founder + ", name=" + name + ", employees=" + employees + ")"; } }
@Cleanup
@Cleanup Lombok 代码:
public void testCleanUp() { try { @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(new byte[] {'Y','e','s'}); System.out.println(baos.toString()); } catch (IOException e) { e.printStackTrace(); } }
@Cleanup
:作用于变量,自动关闭资源,仅针对实现了 java.io.Closeable
接口的对象有效。
等价于 Java 源码:
public void testCleanUp() { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { baos.write(new byte[]{'Y', 'e', 's'}); System.out.println(baos.toString()); } finally { baos.close(); } } catch (IOException e) { e.printStackTrace(); } }
@Synchronized
@Synchronized Lombok 代码:
private DateFormat format = new SimpleDateFormat("MM-dd-YYYY"); @Synchronized public String synchronizedFormat(Date date) { return format.format(date); }
@Synchronized
:作用于方法,可以替换 synchronized 关键字或 lock 锁。
等价于 Java 源码:
private final java.lang.Object $lock = new java.lang.Object[0]; private DateFormat format = new SimpleDateFormat("MM-dd-YYYY"); public String synchronizedFormat(Date date) { synchronized ($lock) { return format.format(date); } }
@SneakyThrows
@SneakyThrows Lombok 代码:
@SneakyThrows public void testSneakyThrows() { throw new IllegalAccessException(); }
@SneakyThrows
:作用于方法,对异常进行捕捉并抛出。
等价于 Java 源码:
public void testSneakyThrows() { try { throw new IllegalAccessException(); } catch (java.lang.Throwable $ex) { throw lombok.Lombok.sneakyThrow($ex); } }
Lombok 使用注意点
谨慎使用 @Builder
在类上标注了 @Data
和 @Builder
注解的时候,编译时,lombok 优化后的 Class 中会没有默认的构造方法。在反序列化的时候,没有默认构造方法就可能会报错。
【示例】使用 @Builder
不当导致 json 反序列化失败
@Data @Builder public class BuilderDemo01 { private String name; public static void main(String[] args) throws JsonProcessingException { BuilderDemo01 demo01 = BuilderDemo01.builder().name("demo01").build(); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(demo01); BuilderDemo01 expectDemo01 = mapper.readValue(json, BuilderDemo01.class); System.out.println(expectDemo01.toString()); } }
运行时会抛出异常:
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `io.github.dunwu.javatech.bean.lombok.BuilderDemo01` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (String)"{"name":"demo01"}"; line: 1, column: 2] at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1432) at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1062) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182) at io.github.dunwu.javatech.bean.lombok.BuilderDemo01.main(BuilderDemo01.java:22)
【示例】使用 @Builder
正确方法
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class BuilderDemo02 { private String name; public static void main(String[] args) throws JsonProcessingException { BuilderDemo02 demo02 = BuilderDemo02.builder().name("demo01").build(); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(demo02); BuilderDemo02 expectDemo02 = mapper.readValue(json, BuilderDemo02.class); System.out.println(expectDemo02.toString()); } }
@Data
注解和继承
使用 @Data
注解时,则有了 @EqualsAndHashCode
注解,那么就会在此类中存在 equals(Object other)
和 hashCode()
方法,且不会使用父类的属性,这就导致了可能的问题。比如,有多个类有相同的部分属性,把它们定义到父类中,恰好 id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,这是因为:lombok 自动生成的 equals(Object other)
和 hashCode()
方法判定为相等,从而导致和预期不符。
修复此问题的方法很简单:
- 使用
@Data
时,加上@EqualsAndHashCode(callSuper=true)
注解。 - 使用
@Getter @Setter @ToString
代替@Data
并且自定义equals(Object other)
和hashCode()
方法。
【示例】测试 @Data
和 @EqualsAndHashCode
@Data @ToString(exclude = "age") @EqualsAndHashCode(exclude = { "age", "sex" }) public class Person { protected String name; protected Integer age; protected String sex; } @Data @EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" }) public class EqualsAndHashCodeDemo extends Person { @NonNull private String name; @NonNull private Gender gender; private String ssn; private String address; private String city; private String state; private String zip; public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender) { this.name = name; this.gender = gender; } public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender, String ssn, String address, String city, String state, String zip) { this.name = name; this.gender = gender; this.ssn = ssn; this.address = address; this.city = city; this.state = state; this.zip = zip; } public enum Gender { Male, Female } } @Test @DisplayName("测试 @EqualsAndHashCode") public void testEqualsAndHashCodeDemo() { EqualsAndHashCodeDemo demo1 = new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "xxx", "xxx", "xxx", "xxx"); EqualsAndHashCodeDemo demo2 = new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "ooo", "ooo", "ooo", "ooo"); Assertions.assertEquals(demo1, demo2); Person person = new Person(); person.setName("张三"); person.setAge(20); person.setSex("男"); Person person2 = new Person(); person2.setName("张三"); person2.setAge(18); person2.setSex("男"); Person person3 = new Person(); person3.setName("李四"); person3.setAge(20); person3.setSex("男"); Assertions.assertEquals(person2, person); Assertions.assertNotEquals(person3, person); }
上面的单元测试可以通过,但如果将 @EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" })
注掉就会报错。