Download an Excel file in Micronaut App
Learn how to download an excel file with Micronaut and Spreadsheet Builder library.
Authors: Sergio del Amo
Micronaut Version: 2.0.0.RC1
1 Getting Started
This guide uses Micronaut 2.x. You can read this tutorial for Micronaut 1.x. |
In this guide, we are going to demonstrate Micronaut file transfer capabilities by creating an app which downloads an excel file with a list of books.
If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

1.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
1.2 Solution
We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/micronaut-guides/micronaut-file-download-excel.git
Then, cd
into the complete
folder which you will find in the root project of the downloaded/cloned project.
2 Writing the App
2.1 Books
Create Book
POJO:
package example.micronaut;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.micronaut.core.annotation.Introspected;
import javax.validation.constraints.NotBlank;
@Introspected
public class Book {
@NonNull
@NotBlank
private String isbn;
@NonNull
@NotBlank
private String name;
public Book() {
}
public Book(@NonNull @NotBlank String isbn, @NonNull @NotBlank String name) {
this.isbn = isbn;
this.name = name;
}
@NonNull
public String getIsbn() {
return isbn;
}
public void setIsbn(@NonNull String isbn) {
this.isbn = isbn;
}
@NonNull
public String getName() {
return name;
}
public void setName(@NonNull String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
if (!isbn.equals(book.isbn)) return false;
return name.equals(book.name);
}
@Override
public int hashCode() {
int result = isbn.hashCode();
result = 31 * result + name.hashCode();
return result;
}
}
Create an interface to encapsulate Book retrieval.
package example.micronaut;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.micronaut.context.annotation.DefaultImplementation;
import java.util.List;
@DefaultImplementation(BookRepositoryImpl.class)
public interface BookRepository {
@NonNull
List<Book> findAll();
}
Create a bean which implements the previous interface
package example.micronaut;
import edu.umd.cs.findbugs.annotations.NonNull;
import javax.inject.Singleton;
import java.util.List;
import java.util.Arrays;
@Singleton (1)
public class BookRepositoryImpl implements BookRepository {
@NonNull
@Override
public List<Book> findAll() {
Book buildingMicroservices = new Book("1491950358", "Building Microservices");
Book releaseIt = new Book("1680502395", "Release It!");
Book cidelivery = new Book("0321601912", "Continuous Delivery:");
return Arrays.asList(buildingMicroservices, releaseIt, cidelivery);
}
}
1 | To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton |
2.2 Spreadsheet Builder
Add a dependency to Spreadsheet builder
Spreadsheet builder provides convenient way how to read and create MS Excel OfficeOpenXML Documents (XSLX) focus not only on content side but also on easy styling.
dependencies {
...
..
.
implementation "builders.dsl:spreadsheet-builder-poi:2.1.0"
}
2.3 Excel Creation
Create a interface to encapsulate Excel generation:
package example.micronaut;
import java.util.List;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.micronaut.context.annotation.DefaultImplementation;
import io.micronaut.http.server.types.files.SystemFile;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@DefaultImplementation(BookExcelServiceImpl.class)
public interface BookExcelService {
String SHEET_NAME = "Books";
String HEADER_ISBN = "Isbn";
String HEADER_NAME = "Name";
String HEADER_EXCEL_FILE_SUFIX = ".xlsx";
String HEADER_EXCEL_FILE_PREFIX = "books";
String HEADER_EXCEL_FILENAME = HEADER_EXCEL_FILE_PREFIX + HEADER_EXCEL_FILE_SUFIX;
@NonNull
SystemFile excelFileFromBooks(@NonNull @NotNull List<@Valid Book> bookList); (1)
}
1 | SystemFile is used as the return value of a route execution to indicate the given file should be downloaded by the client instead of displayed. |
Externalize your styles configuration into a class implementing builders.dsl.spreadsheet.builder.api.Stylesheet
interface to maximize code reuse.
package example.micronaut;
import builders.dsl.spreadsheet.api.FontStyle;
import builders.dsl.spreadsheet.builder.api.CanDefineStyle;
import builders.dsl.spreadsheet.builder.api.Stylesheet;
public class BookExcelStylesheet implements Stylesheet {
public static final String STYLE_HEADER = "header";
@Override
public void declareStyles(CanDefineStyle stylable) {
stylable.style(STYLE_HEADER, st -> {
st.font(f -> f.style(FontStyle.BOLD));
});
}
}
Create a bean which generates the excel file.
package example.micronaut;
import builders.dsl.spreadsheet.builder.poi.PoiSpreadsheetBuilder;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.http.server.types.files.SystemFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
@Singleton (1)
public class BookExcelServiceImpl implements BookExcelService {
private static final Logger LOG = LoggerFactory.getLogger(BookExcelServiceImpl.class);
@NonNull
public SystemFile excelFileFromBooks(@NonNull @NotNull List<@Valid Book> bookList) {
try {
File file = File.createTempFile(HEADER_EXCEL_FILE_PREFIX, HEADER_EXCEL_FILE_SUFIX);
PoiSpreadsheetBuilder.create(file).build(w -> {
w.apply(BookExcelStylesheet.class);
w.sheet(SHEET_NAME, s -> {
s.row(r -> Stream.of(HEADER_ISBN, HEADER_NAME)
.forEach(header -> r.cell(cd -> {
cd.value(header);
cd.style(BookExcelStylesheet.STYLE_HEADER);
})
));
bookList.stream()
.forEach( book -> s.row(r -> {
r.cell(book.getIsbn());
r.cell(book.getName());
}));
});
});
return new SystemFile(file).attach(HEADER_EXCEL_FILENAME);
} catch (IOException e) {
if (LOG.isErrorEnabled()) {
LOG.error("File not found exception raised when generating excel file");
}
}
throw new HttpStatusException(HttpStatus.SERVICE_UNAVAILABLE, "error generating excel file");
}
}
1 | To register a Singleton in Micronaut’s application context annotate your class with javax.inject.Singleton |
2.4 Controller
Add Server Side View Rendering and Thymeleaf dependencies:
dependencies {
...
..
.
implementation("io.micronaut.views:micronaut-views-thymeleaf")
}
Create a controller:
package example.micronaut;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.types.files.SystemFile;
import io.micronaut.views.View;
import java.util.HashMap;
import java.util.Map;
@Controller (1)
public class HomeController {
protected final BookRepository bookRepository;
protected final BookExcelService bookExcelService;
public HomeController(BookRepository bookRepository, (2)
BookExcelService bookExcelService) {
this.bookRepository = bookRepository;
this.bookExcelService = bookExcelService;
}
@View("index") (3)
@Get
public Map<String, String> index() {
return new HashMap<>();
}
@Produces(value = "application/vnd.ms-excel")
@Get("/excel") (4)
public SystemFile excel() { (5)
return bookExcelService.excelFileFromBooks(bookRepository.findAll());
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path / |
2 | Constructor injection |
3 | Use @View annotation to specify which template would you like to render the response against. |
4 | You can specify the HTTP verb for which a controller’s action responds to. To respond to a GET request, use io.micronaut.http.annotation.Get |
5 | SystemFile is used as the return value of a route execution to indicate the given file should be downloaded by the client instead of displayed. |
The previous controller index method renders a simple view with a link to download the excel file:
<!DOCTYPE html>
<html>
<head>
<title>Micronaut</title>
</head>
<body>
<p><a href="/excel">Excel</a></p>
</body>
</html>
2.5 Tests
Often, file transfers remain untested in many applications. In this section, you will see how easy is to test that the file downloads but also that the downloaded file contents match our expectations.
Micronaut is test framework agnostic. For this tutorial, we use Spock Framework. Additionally, we use Geb; a browser automation solution.
To use Geb, add dependencies:
testImplementation ("org.gebish:geb-spock:3.4") {
exclude group: "org.codehaus.groovy", module: "groovy-all"
}
testRuntime "org.seleniumhq.selenium:selenium-chrome-driver:3.141.59"
testImplementation "org.seleniumhq.selenium:selenium-support:3.141.59"
Create a Geb Configuration file. We configure some chrome options to control the download path.
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
ChromeOptions options = new ChromeOptions()
if ( System.getProperty('download.folder') ) {
options.setExperimentalOption("prefs", [
"profile.default_content_settings.popups": 0, (1)
"download.default_directory": System.getProperty('download.folder') (2)
])
}
environments {
chrome {
driver = { new ChromeDriver(options) }
}
chromeHeadless {
driver = {
options.addArguments('headless')
new ChromeDriver(options)
}
}
}
1 | Disable confirmation popups |
2 | Configure the download folder |
Geb uses the Page concept pattern - The Page Object Pattern gives us a common sense way to model content in a reusable and maintainable way. Create a Geb Page to encapsulate the Excel link:
package example.micronaut
import geb.Page
class HomePage extends Page {
static at = { title == 'Micronaut' }
static url = '/'
static content = {
excelLink { $('a', text: 'Excel', 0) }
}
void downloadExcel() {
excelLink.click()
}
}
Install webdriver-binaries Gradle plugin; a plugin that downloads and caches WebDriver binaries specific to the OS the build runs on.
plugins {
...
..
id "com.github.erdi.webdriver-binaries" version "2.2"
}
webdriverBinaries {
chromedriver {
version = '83.0.4103.39'
}
}
tasks.withType(Test) {
systemProperty "geb.env", System.getProperty('geb.env') (1)
systemProperty "download.folder", System.getProperty('download.folder') (2)
beforeTest { descriptor -> logger.quiet " -- $descriptor" }
testLogging {
events "passed", "skipped", "failed"
exceptionFormat 'full'
}
}
1 | Pass system property geb.env to the tests. |
2 | Pass system property download.folder to the tests. |
Create a test which verifies the Excel file is downloaded and the content matches our expectations.
package example.micronaut
import builders.dsl.spreadsheet.query.api.SpreadsheetCriteria
import builders.dsl.spreadsheet.query.api.SpreadsheetCriteriaResult
import builders.dsl.spreadsheet.query.poi.PoiSpreadsheetCriteria
import geb.spock.GebSpec
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.annotation.MicronautTest
import spock.lang.IgnoreIf
import spock.util.concurrent.PollingConditions
import javax.inject.Inject
@MicronautTest (1)
class DownloadExcelSpec extends GebSpec {
@Inject
EmbeddedServer embeddedServer (2)
@IgnoreIf({ !sys['download.folder'] || sys['geb.env'] != 'chrome' })
def "books can be downloaded as an excel file"() {
given:
PollingConditions conditions = new PollingConditions(timeout: 5)
browser.baseUrl = "http://localhost:${embeddedServer.port}" (3)
when:
browser.to HomePage
then:
browser.at HomePage
when: 'clicking excel button'
String expectedPath = System.getProperty('download.folder') + "/" + BookExcelService.HEADER_EXCEL_FILENAME
File outputFile = new File(expectedPath)
browser.page(HomePage).downloadExcel()
then: 'an excel file is downloaded'
conditions.eventually { outputFile.exists() }
when: 'if we search for a row with a particular value (Building Microservices)'
SpreadsheetCriteria query = PoiSpreadsheetCriteria.FACTORY.forFile(outputFile)
SpreadsheetCriteriaResult result = query.query( { workbookCriterion ->
workbookCriterion.sheet(BookExcelService.SHEET_NAME, { sheetCriterion ->
sheetCriterion.row({ rowCriterion ->
rowCriterion.cell({ cellCriterion ->
cellCriterion.value('Building Microservices')
})
})
})
})
then: 'a row is found'
result.cells.size() == 1
cleanup:
outputFile?.delete()
}
}
1 | Annotate the class with @MicronatTest to let Micronaut starts the embedded server and inject the beans. More info: https://micronaut-projects.github.io/micronaut-test/latest/guide/index.html |
2 | Inject the Embedded Server. |
3 | The EmbeddedServer interface provides the URL of the server under test which runs on a random port. We use this port to set Geb base url. |
To run the tests:
$ ./gradlew -Dgeb.env=chrome -Ddownload.folder=/Users/sdelamo/Downloads test
$ open build/reports/tests/test/index.html
3 Running the app
To run the application use the ./gradlew run
command which will start the application on port 8080.
4 Next Steps
Read more about File Transfers support inside Micronaut.