Using lombok to write elegant Bean objects

Keywords: Programming Lombok Java JSON Attribute

Using java to write code, nine out of ten are writing Java classes to build Java objects. lombok has said a lot before, but after so many years of use, it still feels like there are many skills to use.

Undoubtedly, using lombok, the java code is elegant, and the class created by using Lombok is the same as that created by ordinary java encoding.

But is that enough? In fact, lombok has many annotations that make this java class more elegant when used.

This article compares ORM entity class, Builder pattern tool class, Wither tool class and Accessors tool class.

First of all, there are no advantages or disadvantages in different ways, but in different application scenarios, it will become very wonderful.

ORM entity class

When a java Bean class is an ORM entity class, or a mapping class of xml and json, it needs these characteristics:

  • Having a parametric constructor
  • Have setter methods for deserialization;
  • Have getter methods for serialization.

So the simplest case is:

@Data
public class UserBean{
  private Integer id;
  private String userName;
}
  • Recall that the Data annotation is equivalent to assembling @Getter@Setter@RequiredArgsConstructor@ToString@EqualsAndHashCode

So, as an entity class, or a serialized Bean class, that's enough.

Builder

Constructor patterns are frequently used in many tool classes.

package com.pollyduan.builder;

import lombok.Builder;

@Builder
public class UserBean {
	  private Integer id;
	  private String userName;
}

What did it do?

  • It creates a private parametric constructor. This means that no parametric constructor exists; it also means that this class cannot construct objects directly.
  • It creates a method with the same name for each attribute for assignment instead of setter, and the return value of the method is the object itself.

Use:

UserBean u=UserBean.builder()
	.id(1001)
	.userName("polly")
	.build();
System.out.println(u);

It's not bad, but it's amalgamated. Because this Bean doesn't have a getter method, the data in it can't be used directly. It's no use just saying it's no use. Keep on executing and you'll find that the output is this: com.pollyduan.builder.UserBean@20322d26, and you can't even see what's going on. Therefore, Builder offers a possibility that more needs to be done for practical use.

So, we need to add the @ToString() annotation for testing convenience, and we will output UserBean(id=1001, userName=polly)

In a different way, you might think, instead of adding a ToString annotation, I'll turn it into a json output:

UserBean u=UserBean.builder()
	.id(1001)
	.userName("polly")
	.build();
ObjectMapper mapper=new ObjectMapper();
System.out.println(mapper.writeValueAsString(u));

Unfortunately, you will receive the following exceptions:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.pollyduan.builder.UserBean and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

See no properties discovered. Yes, the tool class can't find properties because there is no getter, so we can add @Getter.

package com.pollyduan.builder;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class UserBean {
	  private Integer id;
	  private String userName;
}

Serialization to json is okay, so what about deserializing from a json to an object?

@Builder
@Getter
@Setter
public class UserBean {
	  private Integer id;
	  private String userName;
}

Or not, if unintentionally, you will encounter com. fasterxml. jackson. databind. exc. Invalid Definition Exception: Cannot construct instance of com. pollyduan. builder. UserBean (no creators, default construct, exist): cannot deserialize from Object value (no delegate-or property-based creator)

As explained earlier, adding @Setter is not enough. He also needs a parametric constructor. So, can I do the following?

package com.pollyduan.builder;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class UserBean {
	  private Integer id;
	  private String userName;
}

Similarly, no parametric constructors can be used directly when Data is used, but because of the introduction of full parametric constructors by Builder, no parametric constructors are available according to the native rules of java. Then add a parametric constructor

@Builder
@Data
@NoArgsConstructor

Unfortunately, the Builder made another mistake, and it couldn't find the full-parameter constructor. Well, the final effect is as follows:

@Builder
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor
  • Pay attention to the access level of the full-parameter constructor, and don't break the Builder's rules.

Further, see the following example:

package com.pollyduan.builder;

import java.util.List;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor
public class UserBean {
	  private Integer id;
	  private String userName;
	  private List<String> addresses;
}

Think about it. We also need a new Array List outside and build it in. It's not comfortable to use. lombok provides another annotation to be used in conjunction with @Singular, as follows:

package com.pollyduan.builder;

import java.util.List;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor
public class UserBean {
	  private Integer id;
	  private String userName;
	  @Singular
	  private List<String> favorites;
}

Then you can manipulate the list in this way.

UserBean u = UserBean.builder()
	.id(1001)
	.userName("polly")
	.favorite("music")
	.favorite("movie")
	.build();

Is it convenient? It also provides a clearXXX method to empty the collection.

There is also a small pit, if we add an example attribute, and then give it a default value:

@Builder
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor
public class UserBean {
	  private Integer id;
	  private String userName;
	  private String example="123456";
}

Take a test and see:

UserBean u = UserBean.builder()
	.id(1001)
	.userName("polly")
	.build();
System.out.println(u.toString());

Output: UserBean(id=1001, userName=polly, example=null)

Hey, isn't it? What about the default value? This is explained by the principle of Builder, which actually sets a set of values for the list of attributes separately, and then creates objects using a full-parameter constructor. Then, the default value is on the Bean, not on the Builder, so the Builder is not assigned, its value is null, and finally all the attributes are copied to the UserBean, so null overrides the default value.

How can Builder entities have default values? Just add the @Default annotation level to the field.

package com.pollyduan.builder;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Builder.Default;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor
public class UserBean {
	  private Integer id;
	  private String userName;
	  @Default
	  private String example="123456";
}

Reexecute the test case, output: UserBean(id=1001, userName=polly, example=123456), OK, no problem.

Wither

Objects are built in the way of wither, which is quite common in Objective-C.

The applicable scenario is to use several necessary parameters to construct objects, other parameters, and dynamic assembly. For example, we build an ApiClient whose username and password are required, whose ApiService address has a default value, and then we can customize the address ourselves.

package com.pollyduan.wither;

import lombok.AllArgsConstructor;
import lombok.experimental.Wither;

@Wither
@AllArgsConstructor //WITHER NEED IT.
public class ApiClient {
	private String appId;
	private String appKey;
	private String endpoint="http://api.pollyduan.com/myservice";
}

How to use it?

@Test
public void test1() {
	ApiClient client1=new ApiClient(null, null,null);
	System.out.println(client1);

	Object client2 = client1.withAppId("10001")
		.withAppKey("abcdefg")
		.withEndpoint("http://127.0.0.1/");
	System.out.println(client2);
}

It's strange to use null to initialize an object by default. Like Builder, Wither offers possibilities and needs to be adjusted for actual use.

We can set up a constructor for the required parameters as follows:

package com.pollyduan.wither;

import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Wither;

@RequiredArgsConstructor
@Wither
@AllArgsConstructor
public class ApiClient {
	@NonNull
	private String appId;
	@NonNull
	private String appKey;
	private String endpoint="http://api.pollyduan.com/myservice";
}

In this way, it can be used as follows:

	@Test
	public void test1() {
		ApiClient client1=new ApiClient("10001", "abcdefgh");
		System.out.println(client1);
		
		Object client2 = client1.withEndpoint("http://127.0.0.1/");
		System.out.println(client2);
	}

Is it much more elegant? Chain grammar is also used in practice:

ApiClient client1=new ApiClient("10001", "abcdefgh")
	withEndpoint("http://127.0.0.1/");

In addition, there is a small detail. The output of the previous example is as follows:

com.pollyduan.wither.ApiClient@782830e
com.pollyduan.wither.ApiClient@470e2030

This log indicates that the object returned with() is not the original object, but a new one, which is very important.

Accessors

Accessor mode is to add a convenient accessor to an ordinary Bean, including reading and writing.

It has two working modes, fluent and chain. Examples are given:

package com.pollyduan.accessors;

import lombok.Data;
import lombok.experimental.Accessors;

@Accessors(fluent = true)
@Data
public class UserBean {
	private Integer id;
	private String userName;
	private String password;
	
}

Use code:

UserBean u=new UserBean()
	.id(10001)
	.userName("polly")
	.password("123456");

u.userName("Tom");
System.out.println(u.userName());

This is similar to Builder, but smaller, and does not affect the reading and writing of attributes, except that getter and setter are replaced by the same name string of attributes.

Another model is chain:

UserBean u=new UserBean()
	.setId(10001)
	.setUserName("polly")
	.setPassword("123456");

u.setUserName("Tom");
System.out.println(u.getUserName());

As you can see, the difference between fluent is the use of getter and setter.

Posted by Gasolene on Mon, 22 Apr 2019 11:36:35 -0700