آشنایی با Lombok

Lombok ابزاری است که اخیرا توسط توسعهدهندگان جاوا به میزان زیادی استفاده میشود و کسانی که از این ابزار استفاده میکنند، پس از مدتی، کد زدن بدون Lombok را نمیتوانند تصور کنند.
جاوا زبان فوقالعادهای است، اما گاهی اوقات مجبور میشوید برای کارهای معمول خود یا سازگاری با برخی چارچوبها، مجموعهکدهایی تکراری را به کدهای خود اضافه کنید. این مجموعهکدها معمولا ارزشی برای منطق برنامه شما ایجاد نمیکنند اما به هر حال ناگزیر به ایجاد آنها هستید و اینجا، جایی است که lombok کار شما را راحت میکند. از جمله مجموعهکدهای تکراری، میتوان به setterها و getterها، constructorهای متعدد، متدهای equals و hashCode، تولید خودکار کدهای برخی الگوهای طراحی و … اشاره کرد. ممکن است تصور کنید همه این کارها امروزه توسط IDEهای مدرن انجام میشوند، اما عجله نکنید! lombok فراتر از این حرفهاست.
نحوه کار lombok بدین صورت است که در زمان compile کدهای مورد نیاز برای کلاسهای شما را تولید میکند. برای این کار، تنها کاری که لازم است انجام دهید استفاده از annotationهای معرفی شده lombok است. پس قرار نیست چیزی به کدهای خود اضافه کنید به جز چند annoatation! ضمنا ممکن است خدای ناکرده فکر کرده باشید که lombok برای انجام کارهای خود از reflection استفاده میکند. در پاسخ، شما را به اولین جمله این پاراگراف ارجاع میدهیم. پس نگران performance نباشید!
قبل از این که به مَثَل Talk is cheap, Show me the code عمل کنیم، بیاید به مهمترین کارهایی که lombok با چند annoatation ساده برای ما انجام میدهد نگاهی بیندازیم تا جانی تازه کنیم!
- ساخت خودکار getter و setter و constructorها خیلی بهتر از IDEها
- ساخت DTOها و value classها
- تولید متدهای hashCode و toString خیلی بهتر از IDEها
- الگوی طراحی builder
- بررسی CheckedException
- آزادسازی منابع گرفتهشده از سیستم
- گذاشتن logger برای کلاس
- نوشتن متدهای thread-safe
برای استفاده از lombok نیاز است که شما آن را به پروژهی خود معرفی کنید.
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.2</version> <scope>provided</scope> </dependency>
تصویر فوق، نحوه انجام این کار را با استفاده از maven به نمایش میگذارد که در فایل pom.xml پروژه اضافه میشود. این وابستگی annotationهای lombok را در زمان توسعه در اختیار شما قرار میدهد و در زمان اجرا اقدام به تولید خودکار کدهای مورد نیاز میکند. اما اگر از IDEها استفاده میکنید، نیاز است که lombok را به عنوان یک plugin به آنها معرفی کنید تا خشمگین نشوند! به سایت lombok سری بزنید تا نحوه افزودن آن را به IDEای که از آن استفاده میکنید یاد بگیرید. اما اجمالا اگر از eclipse استفاده میکنید کافی است که jar فایل lombok را دانلود کنید و آن را اجرا نمایید. اگر از intelij استفاده میکنید باید lombok را به پلاگینهای intelij اضافه کنید.
نحوه اضافه نمودن lombok به IDEهای مختلف و افزودن آن به عنوان یک وابستگی به پروژه توسط ابزارهای مختلفی مثل gradle و maven به صورت دقیق و بسیار ساده در سایت lombok توضیح داده شده است. ما در اینجا برای تمرکز بر استفاده از امکانات lombok از ذکر این جزئیات صرفنظر میکنیم و این کار را بسته به این که شما از چه محیط توسعه و ابزاری برای مدیریت وابستگی استفاده میکنید به عهده خودتان میگذاریم.
خلاصی از شر setter و getter و constructor
در جاوا شما ناگزیرید که از encapsulation استفاده کنید و به propertyهای یک شی از طریق متدهای setter و getter دسترسی داشته باشید، بسیاری از فریمورکها شما را مجبور به این کار میکنند و اصطلاحا کار آنها وابسته به الگوی java-bean است (کلاسی با یک constructor خالی و یک setter و getter برای هر property). این کار آنقدر معمول و رایج است که همه IDEها امکان تولید خودکار این متدها را برای شما فراهم کردهاند. اما در نظر داشته باشید که این کدها باید در پروژه شما باقی بمانند و احتیاج به مراقبت و نگهداری دارند! مثلا اگر نام یک ویژگی تغییر کند یا ویژگی جدیدی اضافه شود، متدهای شما نیز دستخوش تغییر میشوند.
کلاس زیر به عنوان یک entity برای JPA در نظر گرفته شده است. اگر کدهای مربوط به setter و getter را اضافه کنیم، مجموعه کدهای کمارزشی داریم که ربط خاصی به منطق برنامه ما نخواهند داشت. این طور نیست؟ یک کاربر نام و نام خانوادگی و سن دارد، همین.
@Entity public class User implements Serializable { private @Id Long id; // will be set when persisting private String firstName; private String lastName; private int age; public User() { } public User(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } // getters and setters: ~30 extra lines of code }
حال بیاید معادل همین کلاس را با lombok ببینیم.
@Entity @Getter @Setter @NoArgsConstructor // <--- THIS is it public class User implements Serializable { private @Id Long id; // will be set when persisting private String firstName; private String lastName; private int age; public User(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } }
با اضافه کردن Getter@ و Setter@، به lombok میگوییم که برای همه فیلدهای کلاس متدهای مربوطه را تولید کند. NoArgsConstructor@ نیز به تولید یک constructor خالی منجر میشود. این تمام کاری است که باید انجام دهید و در ضمن نگران اضافه شدن ویژگیهای دیگر یا تغییر نامها نیز نخواهید بود. همچنین میتوانید از این annotaionها در کنار هر property استفاده کنید تا lombok امکانات بیشتری در اختیار شما قرار دهد. مثلا میتوانید سطح دسترسی پیشفرض هر property را تغییر دهید. به عنوان مثال ممکن است به دلایلی دوست داشته باشید سطح دسترسی فیلد id در سطح package باشد یا بخواهید آن را protected کنید.
privaet @id @Setter(AccessLevel.PROTECTED) Long id;
Value classes / DTO
در زمان توسعه، بسیاری از اوقات برای نمایش دادههای پیچیده در قالب کلاس، از به اصطلاح Value class یا Data Transfer Objectها استفاده میکنیم. مثلا برای نمایش نتیجه عملیات لاگین ممکن است از کلاس زیر استفاده کنیم. میخواهیم این فیلدها خالی نباشند و غیرقابل تغییر باشند پس احتیاجی به setter نداریم.
public class LoginResult { private final Instant loginTs; private final String authToken; private final Duration tokenValidity; private final URL tokenRefreshUrl; // constructor taking every field and checking nulls // read-only accessor, not necessarily as get*() form }
کدهایی که باید بنویسیم به صورت کامنت آمدهاند اما با lombok این کدها به صورت زیر در میآیند.
@RequiredArgsConstructor @Accessors(fluent = true) @Getter public class LoginResult { private final @NonNull Instant loginTs; private final @NonNull String authToken; private final @NonNull Duration tokenValidity; private final @NonNull URL tokenRefreshUrl; }
RequiredArgsConstructor@ برای هر فیلد final یک constructor به کلاس اضافه میکند (برای اضافه کردن constructor به فیلدهای غیرfinal از AllArgsConstructor@ استفاده میکنیم).
NonNull@ یک null-checking به constructorها اضافه خواهد کرد و در صورت null بودن فیلد، NullPoinerException پرتاب میشود.
(Accessors(fluent = true@ باعث میشود که متدهای getter برای فیلدها ساخته نشود. مثلا به جای ()getAuthToken متد ()authToken ساخته میشود. این کار برای فیلدهای غیر final هم منجر به ساخته شدن setter های مشابه میشود.
// Imagine fields were no longer final now return new LoginResult() .loginTs(Instant.now()) .authToken("asdasd") . // and so on
ToString And @EqualsAndHashCode@
تولید متدهای ()toString و ()hashCode و ()equals توسط IDEها به طور خودکار انجام میشود. اما ممکن است در ادامه احتیاج به تغییر این متدها داشته باشیم. lombok با اضافه کردن annotationهایی که در سطح کلاس استفاده میشوند کار ما را راحتتر کرده است.
ToString@: این annoatation متد toString را با در نظرگیری تمام فیلدهای کلاس تولید میکند و در صورتی که مدل دادهی شما تغییر کند، احتیاجی به بازنویسی دوباره این متد نخواهید داشت.
EqualsAndHashCode@: این annotation متدهای equals و hashCode را با در نظر گرفتن تمامی فیلدها تولید میکند.
این annotationها امکانات دیگری نیز در اختیار شما میگذارند. به عنوان مثال اگر کلاس شما کلاس دیگری را به ارث برده باشد. می توانید از (EqualAndHashCode(callSuper = true@ استفاده کنید. با این کار متدهای equals و hashCode کلاس پدر نیز در نظر گرفته میشوند.
هنوز کارمان با این annotationها تمام نشده! فرض کنید entity شما فیلد زیر را دارد و میخواهید برای این entityClass متد toString تولید کنید.
@OneToMany(mappedBy = "user") private List<UserEvent> events;
ما دوست نداریم که متد toString، فیلد events را در نظر بگیرد! مشکلی نیست! از امکان exclude به ترتیب زیر استفاده کنید.
@ToString(exclude = {"events"})
در صورتی که بخواهیم کلاس LoginResult متدهای equal و hashCode را برای فیلد authToken در نظر بگیرد به ترتیب زیر عمل میکنیم:
@EqualAndHashCode(of = {"authToken"})
خبر خوب:
اگر بخواهیم از EqualsAndHashCod@ و ToString@ و RequiredArgsContructor@ و Getter@ برای تمامی فیلدها استفاده کنیم و از Setter@ نیز برای فیلدهای غیر final استفاده کنیم ، همه اینها در Data@ جمع شدهاند. Value@ هم عملکرد مشابهی با Data@ دارد اما برای کلاسهای غیر قایل تغییر به کار میرود و از تولید setterها خبری نیست.
الگوی طراحی builder
کلاس زیر را در نظر بگیرید. این کلاس برای config یک کلاینت rest در نظر گرفته شده است.
public class ApiClientConfiguration { private String host; private int port; private boolean useHttps; private long connectTimeout; private long readTimeout; private String username; private String password; // Whatever other options you may thing. // Empty constructor? All combinations? // getters... and setters? }
ما میخواهیم کلاسمان immutable (غیر قابل تغییر) باشد و همان زمانی که ساخته میشود ویژگیهای آن مقدار بگیرد. پس باید از setterها صرف نظر کنیم و کار خود را با constructor به انجام برسانیم اما تعداد parameterها زیاد است. پس ….
میتوانیم از الگوی طراحی builder استفاده کنیم. به کمک lombok کلاس بالا را به شکل زیر بازنویسی میکنیم تا کلاس builder و متدهای شبیه به setter آن ساخته شود.
@Builder public class ApiClientConfiguration { // ... everything else remains the same }
ملاحظه میکنید که کلاس را بدون هیچ annotation دیگری رها کردهایم و میتوانیم به شکل زیر از آن استفاده کنیم.
ApiClientConfiguration config = new ApiClientConfigurationBuilder() .host("api.server.com") .port(443) .useHttps(true) .connectTimeout(15_000L) .readTimeout(5_000L) .username("myusername") .password("secret") .build();
بررسی CheckedException
بسیاری از APIها به گونهای طراحی شدهاند که میتوانند یک یا تعدادی checked-exception را پرتاب کنند و کلاینتی که از API استفاده میکند باید این استثناها را catch کند یا از throws استفاده کند. در بسیاری از موارد این استثناها هیچگاه رخ نمیدهند و ما فقط به خاطر اجبار کامپایلر این استثناها را catch یا throws میکنیم.
public String resourceAsString() { try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); return br.lines().collect(Collectors.joining("\n")); } catch (IOException | UnsupportedCharsetException ex) { // If this ever happens, then its a bug. throw new RuntimeException(ex); //<--- encapsulate into a Runtime ex. } }
با استفاده از @SneakyThrows میتوانیم کد بالا را به صورت تمیزتری دربیاوریم.
@SneakyThrows public String resourceAsString() { try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); return br.lines().collect(Collectors.joining("\n")); } }
اطمینان از آزادسازی منابع
جاوا ۷ مفهومی با نام بلاک try-with-resource را معرفی کرد تا اطمینان حاصل شود که منابع گرفتهشده توسط برنامه اگر واسط AutoClosable را پیادهسازی کرده باشند، به طور خودکار آزاد میشوند. lombok از رویکرد متفاوت و بهتری استفاده میکند. کافی است شما از @CleanUp کنارتعریف هر فیلدی که منبعی از سیستم اشغال خواهد کرد استفاده کنید، آنها به طور خودکار آزاد خواهند شد بدون این که واسط خاصی را پیادهسازی کرده باشند.
@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");
اگر متدی که برای آزادسازی منابع استفاده میشود، نام دیگری غیر از close دارد به صورت زیر عمل کنید.
@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window”);
استفاده از logger ها بدون تعریف آنها!
بسیاری از ما یک logger را به صورت زیر به کلاس اضافه میکنیم
public class ApiClientConfiguration { private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class); // LOG.debug(), LOG.info(), ... }
لاگ زدن بسیار پرکاربرد و با اهمیت است و lombok تعریف logger را آسان نموده است.
@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j public class ApiClientConfiguration { // log.debug(), log.info(), ... }
همانطور که در کد ملاحظه میکنید بسته به این که از چه فریمورکی برای لاگ زدن استفاده میکنید، میتوانید از annotationهای مختلفی استفاده کنید. lombok از بسیاری از logging framework ها پشتیبانی میکند.
متدهای Thread–Safe
در جاوا برای پیادهسازی بخشهای به اصطلاح critical section باید از کلمه کلیدی synchronized استفاده کرد. اما این راه میتواند روش مناسبی نباشد چرا که اگر کلاینتهای دیگر کد از synchronized استفاده کرده باشند، ممکن است برنامه دچار deadlock شود. اینجاست که lombok با @Synchronized به میدان میآید:
@Synchronized public /* better than: synchronized */ void putValueInCache(String key, Object value) { // whatever here will be thread-safe code }
جمعبندی
امکانات جالب و پراستفاده دیگری نیز در lombok وجود دارد که در این مقاله به آن پرداخته نشده است. برای آشنایی بیشتر و کسب یک نگاه عمیقتر اینجا را ملاحظه کنید.
اکثر ویژگیهای توضیح داده شده امکاناتی جهت سازگاری بیشتر با شیوههای مختلف نامگذاری و … برای شما فراهم آورده است که با مراجعه به مستندات lombok در دسترس شما خواهند بود.
امیدواریم که این مقاله به شما انگیزه لازم جهت استفاده از lombok را داده باشد. آن را امتحان کنید و بهرهوری خود را افزایش دهید.