paint-brush
Veri Sınıflarından Excel'e: Ek Açıklamalarla Java Eşleyici Oluşturmaile@sergeidzeboev
2,778 okumalar
2,778 okumalar

Veri Sınıflarından Excel'e: Ek Açıklamalarla Java Eşleyici Oluşturma

ile Sergei Dzeboev18m2023/09/24
Read on Terminal Reader
Read this story w/o Javascript

Çok uzun; Okumak

Esnek hücre stili, formül oluşturma ve dinamik rapor oluşturma için ek açıklamalardan yararlanarak Excel raporları oluşturmak üzere özel bir Java kitaplığı geliştirildi. Süreç, Apache POI'nin kullanılmasını, özel açıklamalar oluşturulmasını ve Excel raporu oluşturmaya yönelik bir hizmetin uygulanmasını içerir. Özel yaklaşım, gelecekteki özelleştirme için kolaylık, hassasiyet ve potansiyel sunar.
featured image - Veri Sınıflarından Excel'e: Ek Açıklamalarla Java Eşleyici Oluşturma
Sergei Dzeboev HackerNoon profile picture
0-item

Bugün mevcut olan çok sayıda kitaplık olmasına rağmen, bazen belirli bir görev için gereken belirli işlevleri sunan bir kitaplık bulmak zor olabilir. Mükemmel kitaplığı aramakla zaman harcamak yerine kendi uygulamanızı oluşturmanızı öneririm; tek bir proje için özel olarak tasarlanmış olsa bile.


Bir keresinde kendimi, rapor oluşturmak için veri sınıflarını kolayca bir Excel belgesine dönüştürebilecek bir kitaplığa ihtiyaç duyarken buldum. Uygun bir kütüphane bulamadığım için özel ihtiyaçlarıma uygun işlevler geliştirmeye karar verdim.


Amacım, Jackson'a benzer bir kitaplık tasarlamaktı; bu kitaplık, veri sınıflarının bir listesini, ek açıklamaların gerektirdiği şekilde bir Excel belgesine dönüştürmek için ek açıklamaları kullanırdı.


Başkalarına fayda sağlayabileceğini veya benzersiz görevleri için kendi haritalayıcılarını oluşturma konusunda onlara ilham verebileceğini umarak oluşturduğum kitaplığı paylaşmak istiyorum. Bunu başarmak için Java'da böyle bir veri eşleyicinin nasıl geliştirileceğini inceleyelim:


bundan:

 void demoReport() { var excelMapper = new ExcelMapperImpl(); var fileName = "demo-out-" + LocalTime.now() + ".xlsx"; List<Demo> demos = generateDemos(); try (Workbook workbook = excelMapper.createWorkbookFromObject(demos); var fileOutputStream = new FileOutputStream(fileName)) { workbook.write(fileOutputStream); } }


Ek Açıklamaları Tanımlama

Excel eşlemesi için gerekli olan ana unsurları tanımlayalım. Özünde bir Excel sütununa ihtiyacımız var. Raporun bu temel bileşeni, her satırda sütun adını ve karşılık gelen değeri açıkça göstermelidir.


Ayrıca, değerleri kullanmamıza ve sonuçları dinamik olarak sunmamıza olanak tanıyan formül hücreleri desteğini de dahil etmeliyiz. Sütunun sonunda, ister bir ortalamayı, ister toplamı, ister son kullanıcı için ilgili başka bir ölçümü temsil etsin, bir sonuç formülü önemlidir.


Salt veri hücrelerinin ötesinde, hücre stilini kolayca yönetecek özellikleri de entegre etmeliyiz.


Temel unsurları belirledikten sonra bir sonraki adım gerekli ek açıklamaları hazırlamaktır. İlk ek açıklama, hücre stiliyle ilgili meta verileri yerleştirecektir. Bu ek açıklama, varsayılan değerleriyle birlikte temel nitelikleri kapsayacaktır:

 @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelStyle { ExcelColumnDataFormat cellTypePattern() default ExcelColumnDataFormat.NONE; IndexedColors cellColor() default IndexedColors.AUTOMATIC; boolean isWrapText() default false; boolean isCentreAlignment() default false; boolean isFramed() default true; ExcelColumnFont fontName() default ExcelColumnFont.DEFAULT; short fontSize() default -1; boolean isFontBold() default false; ExcelColumnCellTextColor fontColor() default ExcelColumnCellTextColor.AUTOMATIC; }


Rapor oluşturma için hayati önem taşıyan birincil stil öğeleri, özellikler olarak aktarılır. Bunu takiben, bir Excel sütunu açıklamasının başlatılması gerçekleştirilebilir:

 @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcel { String[] applyNames() default {}; int position(); ColumnExcelStyle headerStyle() default @ColumnExcelStyle( fontColor = ExcelColumnCellTextColor.BLACK, isCentreAlignment = true, isFontBold = true, fontSize = 14, isWrapText = true); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }


Bu ek açıklama, olası sütun adlarını (Excel'den eşleme için) ve zorunlu bir alan olan ' position kapsar. Bu, sütunun yerleşimini belirleyecek ve formül hesaplamalarında etkili olacaktır. Ek olarak, hem başlığın hem de hücrenin stilini detaylandıracaktır.


Harika. Şimdi Excel formüllerine özel bir açıklama formüle edelim. Satırın konumuna bağlı dinamik bir formül öngören bu açıklama, yöntemlere özel olacaktır:

 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelFormula { String name() default ""; int position(); ColumnExcelStyle headerStyle() default @ColumnExcelStyle( fontColor = ExcelColumnCellTextColor.BLACK, isCentreAlignment = true, isFontBold = true, fontSize = 14, isWrapText = true); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }


Son olarak, genellikle Excel'deki son satırı kaplayan ve bir sütunun kümülatif sonucunu özetleyen veya gösteren sonuç formülü için bir ek açıklama ekleyelim. Formülün ek açıklamasının aynı zamanda onun yalnızca yöntemlere uygulanabilirliğini zorunlu kıldığı göz önüne alındığında:

 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ColumnExcelTotalFormula { boolean useValue() default false; int position(); ColumnExcelStyle cellStyle() default @ColumnExcelStyle; }


Apache POI ile Excel Raporlama Hizmeti Oluşturma

Gerekli tüm ek açıklamaların oluşturulmasından sonra bir sonraki adım bir hizmet sınıfının oluşturulması olur. Kullanılan çekirdek kitaplık, .xls ve .xlsx dosyalarıyla çalışmak için etkili olan Apache POI olacaktır. Bu sınıf, bir Excel raporu oluşturmak için ek açıklamaların niteliklerini kullanacaktır.


Birincil yöntem, nesnelerin bir listesini girdi olarak kabul eder ve hazırlanmış bir çalışma kitabını döndürür.


Daha fazla esneklik sağlamak amacıyla, rapor oluşturmak için hem dosya adının hem de sayfa adının belirtilmesini mümkün kılan aşırı yüklenmiş bir yöntem tanıtılacaktır:

 <T> Workbook createWorkbookFromObject(List<T> reportObjects) { return createWorkbookFromObject(reportObjects, 0, "Report"); } <T> Workbook createWorkbookFromObject(List<T> reportObjects, int startRowNumber, String sheetName) { ... }


Yansımayı kullanarak sınıf hakkında bilgi çıkarmak için dizideki herhangi bir öğe seçilir. Sınıf detaylarına ulaşıldıktan sonra ilk sıra oluşturulabilir. Ek açıklamadaki verilerin kullanılması, ilgili adlara sahip hücrelerin oluşturulmasına olanak tanır.


Bir ad yoksa, sınıf alanının adı alternatif olarak kullanılabilir:

 private <T> void createHeaderFromDeclaredExcelColumns(Row row, Class<T> clazz, PropertyDescriptor propertyDescriptor) { try { Field field = clazz.getDeclaredField(propertyDescriptor.getName()); ColumnExcel columnExcel = field.getDeclaredAnnotation(ColumnExcel.class); if (nonNull(columnExcel)) { String headerName = columnExcel.applyNames().length > 0 ? columnExcel.applyNames()[0] : field.getName(); createHeader(row, columnExcel.position(), headerName, columnExcel.headerStyle()); } } catch (NoSuchFieldException e) { log.debug(e.getLocalizedMessage()); } }


Başlığı şekillendirirken her hücreye bir stil atamayı unutmayın. Stil parametreleri @ColumnExcelStyle ek açıklamasından türetilebilir:

 private void createHeader(Row row, int position, String name, ColumnExcelStyle columnExcelStyle) { Cell cell = row.createCell(position); cell.setCellValue(name); setCellFormatting(cell, columnExcelStyle); row.getSheet().autoSizeColumn(cell.getColumnIndex()); }


Daha sonra süreç, sağlanan dizideki verilere dayalı olarak raporda satırlar oluşturmaya dönüşür. Veriler üzerinde yineleme yapılarak ardışık satırlar oluşturulur:

 for (T report : reportObjects) { Row bodyRow = sheet.createRow(proceedRowNumber); createCellsFromDeclaredExcelColumns(bodyRow, report); proceedRowNumber++; }


Özellik tanımlayıcıları, alanlara doğrudan erişim sağlamak yerine alıcıları kullanmak için tedarik edilir:

 private <T> void createCellsFromDeclaredExcelColumns(Row row, T tObject) { try { PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(tObject.getClass()).getPropertyDescriptors(); for (var propertyDescriptor : propertyDescriptors) { createCellFromDeclaredExcelColumns(row, tObject, propertyDescriptor); } } catch (IntrospectionException ex) { log.debug(ex.getLocalizedMessage()); } }


Özellik tanımlayıcıyla bir hücre oluşturulur:

 private <T> void createCellFromDeclaredExcelColumns(Row row, T tObject, PropertyDescriptor propertyDescriptor) { try { Field field = tObject.getClass().getDeclaredField(propertyDescriptor.getName()); Method readMethod = propertyDescriptor.getReadMethod(); ColumnExcel columnExcel = field.getDeclaredAnnotation(ColumnExcel.class); if (nonNull(columnExcel)) { Class<?> returnType = readMethod.getReturnType(); Cell cell = row.createCell(columnExcel.position()); Object invokeResult = readMethod.invoke(tObject); if (nonNull(invokeResult)) { defineAndAssignCellValue(returnType, cell, invokeResult, readMethod); } setCellFormatting(cell, columnExcel.cellStyle()); } } catch (NoSuchFieldException | InvocationTargetException | IllegalAccessException e) { log.debug(e.getLocalizedMessage()); } }


Şimdi dikkatimizi @ColumnExcelFormula ek açıklamalarını işlemeye çevirelim. Formüller oluşturmak, bir alandan basitçe değer çıkarmaktan biraz daha karmaşıktır. Bir yöntemin daha sonra hücreye atanacak bir formül üretmesi beklenir.


Yöntem tutarlı bir şekilde bir dize döndürmeli ve satır numarasını bağımsız değişken olarak kabul etmeli, böylece bitişik hücrelerden doğru veri kullanımı sağlanmalıdır.


Bu nedenle, hücreyi belirtilen formülle oluşturmadan önce bu koşulların karşılandığını doğrulamak işleyiciye düşer:

 private <T> void createCellFromDeclaredExcelFormula(Row row, T tObject, Method readMethod) throws IllegalAccessException, InvocationTargetException { ColumnExcelFormula columnExcelFormula = readMethod.getDeclaredAnnotation(ColumnExcelFormula.class); if (columnExcelFormula != null) { Class<?> returnType = readMethod.getReturnType(); Cell cell = row.createCell(columnExcelFormula.position()); if (returnType.isAssignableFrom(String.class)) { cell.setCellFormula((String) readMethod.invoke(tObject, row.getRowNum())); } else { log.debug(" Return type for the method: " + readMethod.getName() + " with @ColumnExcelFormula annotation has to be String " + "and now it's: " + returnType.getName() + " method is ignored for the reason"); } setCellFormatting(cell, columnExcelFormula.cellStyle()); } }


Son adım, sonuç sonuçlarını görüntülemek için bir satır oluşturmayı içerir. Daha da önemlisi, işleyiciye iletilen nesnelerin sayısına bakılmaksızın bu satırın yalnızca bir kez oluşturulması gerekir. Bu amaç için statik bir yöntemde bir açıklama yapılması öngörülmektedir.


Bu yöntem hem ilk satırın numarasını hem de hücrenin argüman olarak başlatılacağı geçerli satırı alır.


İlk satırın numarasının sağlanması, yöntemin tüm sütun için toplam sonuçtan yararlanan bir formül tasarlamasına olanak sağlamak açısından hayati öneme sahiptir:

 private <T> void createTotalFormula(Class<T> tClazz, Row row, int firstRowNum) { Method[] methods = tClazz.getDeclaredMethods(); for (Method method : methods) { ColumnExcelTotalFormula columnExcelTotalFormula = method.getAnnotation(ColumnExcelTotalFormula.class); if (columnExcelTotalFormula != null && method.getReturnType().isAssignableFrom(String.class) && method.getParameters().length == 2 && Modifier.isStatic(method.getModifiers()) && !Modifier.isPrivate(method.getModifiers()) ) { String cellFormula = (String) method.invoke(tClazz, firstRowNum, row.getRowNum()); Cell cell = row.createCell(columnExcelTotalFormula.position()); cell.setCellFormula(cellFormula); if (columnExcelTotalFormula.useValue()) { cell = applyFormulasValue(cell); } setCellFormatting(cell, columnExcelTotalFormula.cellStyle()); } } }


Rapor Oluşturma

Ana işlevsellik artık mevcut ve onu çalışırken görmenin zamanı geldi. İşleyişini göstermek için basit bir rapor oluşturalım. Bunun için bir ' Sales ' sınıfı oluşturalım ve gerekli tüm açıklamaları dahil edelim:

 @Data @Accessors(chain = true) public class Sales { @ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE)) private LocalDate date; @ColumnExcel( position = 1, applyNames = {"Sold"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT)) private Integer sold; @ColumnExcel( position = 2, applyNames = {"Price Per Unit (USD)"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = USD)) private Double pricePerUnit; @ColumnExcelFormula( position = 3, name = "Total Sales (USD)", headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = USD)) public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); } @ColumnExcelTotalFormula( position = 0, cellStyle = @ColumnExcelStyle( cellColor = LIGHT_BLUE)) public static String total(int firstRowNum, int lastRowNum) { return "CONCATENATE(\"Total\")"; } @ColumnExcelTotalFormula( position = 1, cellStyle = @ColumnExcelStyle( cellColor = LIGHT_BLUE)) public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; } @ColumnExcelTotalFormula( position = 3, cellStyle = @ColumnExcelStyle( isCentreAlignment = false, cellColor = LIGHT_BLUE, cellTypePattern = USD)) public static String totalSales(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 3).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 3).formatAsString() + ")"; } }


Sınıf üç alandan oluşur: date , sold ve pricePerUnit . Ek olarak, bir satış formülü ve toplamları içeren bir sonuç satırı vardır: unitsSold ve totalSales . Alanlar, sütunun konumunu ve adını belirten @ColumnExcel ek açıklamasını kullanır.


@ColumnExcelStyle ek açıklaması, hem başlığın hem de tek tek veri hücrelerinin stilini tanımlar:

 @ColumnExcel( position = 0, applyNames = {"Date"}, headerStyle = @ColumnExcelStyle( fontColor = WHITE, cellColor = DARK_BLUE, isCentreAlignment = true), cellStyle = @ColumnExcelStyle( cellColor = GREY_25_PERCENT, cellTypePattern = DATE) )


Daha önce tartışıldığı gibi, bir formül oluştururken yöntemin satır numarasını belirten bir parametreyi kabul etmesi gerekir. Bu gereksinim, yöntemin imzasında açıkça görülmektedir:

 public String sales(int rowNum) { return new CellAddress(rowNum, 1).formatAsString() + "*" + new CellAddress(rowNum, 2).formatAsString(); }


Satır numarası ve sütun indeksleri göz önüne alındığında, herhangi bir özel formülün işlenmesi mümkün hale gelir.


Sınıf içinde, sonuç formüllerine yönelik yöntemler statiktir ve iki parametre gerektirir: başlangıç satırının numarası ve bitiş satırının numarası:

 public static String unitsSold(int firstRowNum, int lastRowNum) { return "SUM(" + new CellAddress(firstRowNum, 1).formatAsString() + ":" + new CellAddress(lastRowNum - 1, 1).formatAsString() + ")"; }


Şimdi yöntemi başlatalım:

 void salesReport() { var excelMapper = new ExcelMapperImpl(); var fileName = "sales-out-" + LocalTime.now() + ".xlsx"; List<Sales> sales = List.of( new Sales().setDate(LocalDate.of(2023, 1, 1)) .setSold(50) .setPricePerUnit(10d), new Sales().setDate(LocalDate.of(2023, 1, 2)) .setSold(40) .setPricePerUnit(11d), new Sales().setDate(LocalDate.of(2023, 1, 3)) .setSold(55) .setPricePerUnit(9d); try (Workbook workbook = excelMapper.createWorkbookFromObject(sales); var fileOutputStream = new FileOutputStream(fileName)) { workbook.write(fileOutputStream); } }


Ve oluşturulan raporu inceleyin:

Çözüm

Belirli bir görev için özel bir kütüphane yazmanın kolay olduğu ortaya çıktı. Hazırlanan kütüphane gereksinimleri karşılar ve planlanan işlevselliği içerir. Ek açıklamaları kullanan yaklaşım, hücre stillerinin hızlı ve kolay şekilde özelleştirilmesini, formüllerin değiştirilmesini ve çeşitli veri kaynaklarından dinamik rapor oluşturulmasını kolaylaştırır.


Bu nedenle, bir dahaki sefere uygun bir kütüphane bulmanın zor olduğu durumlarda, kişiselleştirilmiş bir kütüphane geliştirmenin faydalı olabileceği düşünülür.


Doğal olarak bu makaledeki kodun her satırını sunmak mümkün değildi; bu nedenle, yalnızca haritacının çalışması için gerekli olan birincil yöntemler vurgulanmıştır. Kodun tamamı GitHub sayfamda mevcuttur.