This example demonstrates how to use an Excel document as the view technology in a Spring MVC application. Spring integrates with the Apache POI library, which is used for reading and writing Microsoft Office document formats.
In this example, we are going to use AbstractXlsView, a convenient base class for Excel views in the legacy binary XLS format. For the newer XLSX format, Spring provides the subclasses AbstractXlsxView and AbstractXlsxStreamingView.
Deprecation notice: As of Spring Framework 7.0, the view classes in the org.springframework.web.servlet.view.document package, including AbstractXlsView, are deprecated. AbstractXlsxView and AbstractXlsxStreamingView are explicitly marked for removal, which the Spring team plans for version 7.1 or 7.2. The approach shown here still works in Spring 6.x, and in 7.x with deprecation warnings. For new code, see the Alternative Without Document Views section at the end of this tutorial.
Example
Additional Maven Dependency
pom.xml<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.5.1</version>
</dependency>
This example generates the legacy .xls format, so the core poi artifact is sufficient. To generate .xlsx files, use poi-ooxml instead.
Java Config class
In this example, we are going to use BeanNameViewResolver for resolving the Excel view. That means we need to register an implementation of AbstractXlsView as a bean. The bean name will be used as the view name returned from the controller. Also check out this simple example of BeanNameViewResolver.
package com.logicbig.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@EnableWebMvc
@Configuration
@ComponentScan
public class MyWebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(0);
registry.viewResolver(resolver);
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
viewResolver.setOrder(1);
registry.viewResolver(viewResolver);
}
}
Implementing AbstractXlsView
package com.logicbig.example;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.document.AbstractXlsView;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;
import java.util.Map;
@Component("forexView")
public class ForexExcelView extends AbstractXlsView {
@Override
protected void buildExcelDocument(Map<String, Object> model,
Workbook workbook,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
List<CurrencyRate> currencyRates = (List<CurrencyRate>) model.get(
"todayCurrencyRates");
Sheet sheet = workbook.createSheet("Today Currency Rates");
sheet.setFitToPage(true);
int rowCount = 0;
Row header = sheet.createRow(rowCount++);
header.createCell(0).setCellValue("Currency Pair");
header.createCell(1).setCellValue("Bid Price");
header.createCell(2).setCellValue("Ask Price");
header.createCell(3).setCellValue("Date");
for (CurrencyRate currencyRate : currencyRates) {
Row currencyRow = sheet.createRow(rowCount++);
currencyRow.createCell(0)
.setCellValue(currencyRate.getCurrencyPair());
currencyRow.createCell(1)
.setCellValue(currencyRate.getBidPrice().doubleValue());
currencyRow.createCell(2)
.setCellValue(currencyRate.getAskPrice().doubleValue());
currencyRow.createCell(3).setCellValue(
currencyRate.getDateTime().format(
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)));
}
response.setHeader("Content-Disposition",
"attachment; filename=forex-rates.xls");
}
}
package com.logicbig.example;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class CurrencyRate {
private String currencyPair;
private LocalDateTime dateTime;
private BigDecimal askPrice;
private BigDecimal bidPrice;
.............
}
Writing a Spring Controller
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Currency;
import java.util.List;
@Controller
@RequestMapping("/")
public class MyController {
@GetMapping("/")
public String mainView() {
return "main";
}
@GetMapping("/exchangeRates")
public String handleForexRequest(Model model) {
model.addAttribute("todayCurrencyRates",
getTodayForexRates());
return "forexView";
}
private List<CurrencyRate> getTodayForexRates() {
//dummy rates
List<CurrencyRate> currencyRates = new ArrayList<>();
LocalDateTime today = LocalDateTime.now();
List<Currency> currencies =
new ArrayList<>(Currency.getAvailableCurrencies());
for (int i = 0; i + 1 < currencies.size(); i += 2) {
String currencyPair = currencies.get(i) + "/" + currencies.get(i + 1);
CurrencyRate cr = new CurrencyRate();
cr.setCurrencyPair(currencyPair);
cr.setDateTime(today);
BigDecimal bidPrice = new BigDecimal(Math.random() * 5 + 1);
bidPrice = bidPrice.setScale(3, RoundingMode.CEILING);
cr.setBidPrice(bidPrice);
BigDecimal askPrice = new BigDecimal(bidPrice.doubleValue() +
Math.random() * 2 + 0.5);
askPrice = askPrice.setScale(3, RoundingMode.CEILING);
cr.setAskPrice(askPrice);
currencyRates.add(cr);
}
return currencyRates;
}
}
JSP page
src/main/webapp/WEB-INF/views/main.jsp<html>
<body>
<h2>Spring Excel View Example</h2>
<a href="/exchangeRates">Get Today Exchange Rates</a>
</body>
</html>
Output
Clicking the link opens the generated spreadsheet:
Tests
package com.logicbig.example;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class ExcelViewTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.build();
}
@Test
public void mainPageForwardsToJsp() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("main"))
.andExpect(forwardedUrl("/WEB-INF/views/main.jsp"));
}
@Test
public void exchangeRatesIsDownloadedAsXlsAttachment() throws Exception {
mockMvc.perform(get("/exchangeRates"))
.andExpect(status().isOk())
.andExpect(view().name("forexView"))
.andExpect(content().contentType("application/vnd.ms-excel"))
.andExpect(header().string("Content-Disposition",
is("attachment; filename=forex-rates.xls")));
}
@Test
public void workbookBasicTest() throws Exception {
Sheet sheet = downloadSheet();
assertThat(sheet.getSheetName(), is("Today Currency Rates"));
assertThat(cellTexts(sheet.getRow(0)),
contains("Currency Pair", "Bid Price", "Ask Price", "Date"));
assertThat(sheet.getLastRowNum(), greaterThan(0));
}
@Test
public void everyDataRowIsWellFormed() throws Exception {
StreamSupport.stream(downloadSheet().spliterator(), false)
.skip(1)
.forEach(row -> {
assertThat(Arrays.asList(row.getCell(0)
.getStringCellValue()
.split("/")),
hasSize(2));
assertThat(row.getCell(1).getNumericCellValue(),
allOf(greaterThanOrEqualTo(1.0),
lessThanOrEqualTo(6.0)));
assertThat(row.getCell(2).getNumericCellValue(),
greaterThan(row.getCell(1)
.getNumericCellValue()));
assertThat(row.getCell(3).getStringCellValue(),
not(isEmptyOrNullString()));
});
}
private Sheet downloadSheet() throws Exception {
return new HSSFWorkbook(new ByteArrayInputStream(
mockMvc.perform(get("/exchangeRates"))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsByteArray())).getSheetAt(0);
}
private static List<String> cellTexts(Row row) {
return StreamSupport.stream(row.spliterator(), false)
.map(cell -> cell.getStringCellValue())
.collect(Collectors.toList());
}
}
mvn clean test -Dtest="ExcelViewTest" Output (filtered)$ mvn clean test -Dtest="ExcelViewTest" [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ExcelViewTest [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.912 s -- in com.logicbig.example.ExcelViewTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.107 s [INFO] Finished at: 2026-10-06T04:36:01-05:00 [INFO] ------------------------------------------------------------------------
Alternative Without Document Views
Since the document view classes are deprecated, the Spring team recommends rendering the document directly in a web handler, or using a library that provides its own View implementation. A minimal handler-based approach using Apache POI (requires poi-ooxml) looks like this:
@Controller
public class ForexController {
@GetMapping("/forex.xlsx")
public void forex(HttpServletResponse response) throws IOException {
response.setContentType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"forex.xlsx\"");
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
workbook.createSheet("Rates").createRow(0).createCell(0)
.setCellValue("Currency");
workbook.write(response.getOutputStream());
}
}
}
This approach does not depend on any Spring view infrastructure, so it keeps working regardless of when the deprecated classes are removed. Also note that the XLS format is a legacy format. Prefer XLSX unless you must support very old Excel versions.
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.2.0.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- spring-test 7.0.6 (Spring TestContext Framework)
- hamcrest-library 1.3 (Hamcrest library of matcher implementations)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- poi 5.5.1 (Apache POI - Java API To Access Microsoft Format Files)
- JDK 25
- Maven 3.9.11
|