Spring Boot app counts two error requests - spring

I got Spring boot app and used spring aop to count my last 100 rerequestslso I used the Spring boot error page and just added the error template to the code and its works. the problem is that the error page is counted twice. I guess it counted /error and some wrong url like /somewrongUrl. how can I solve this?
#Aspect
#Component
#Slf4j
public class RequestLoggingAspect {
private List<HttpModel> requests = new ArrayList<>();
#Before("within(#org.springframework.stereotype.Controller *)")
public void logRequest(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
HttpModel httpModel = new HttpModel();
httpModel.setMethod(request.getMethod());
httpModel.setUri(request.getRequestURI());
httpModel.setStatusCode(response.getStatus());
httpModel.setTimeStamp(getTime(request));
if(!httpModel.getUri().equals("/getLogg")) {
requests.add(httpModel);
}
}
public List<HttpModel> getRequests() {
int size = requests.size();
if (size > 100) {
return requests.subList(size - 100, size);
}
return requests;
}
public String getTime(HttpServletRequest request) {
long time = request.getDateHeader("Date");
if (time == -1) {
time = Instant.now().toEpochMilli();
}
Date date = new Date(time);
SimpleDateFormat dateFormat = new SimpleDateFormat("kk:mm dd.MM.yyyy");
return dateFormat.format(date);
}

Related

more than one 'primary' service instance suppliers found during load balancing (spring boot/cloud)

I'm currently updating from Spring boot 2.2.x to 2.6.x + legacy code, it's a big jump so there were multiple changes. I'm now running into a problem with load balancing through an api-gateway. I'll apologize in advance for the wall of code to come. I will put the point of failure at the bottom.
When I send in an API request, I get the following error:
more than one 'primary' bean found among candidates: [zookeeperDiscoveryClientServiceInstanceListSupplier, serviceInstanceListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]
it seems that the zookeeperDiscovery and retryAware suppliers are loaded through the default serviceInsatnceListSupplier, which has #Primary over it. I thought would take precedence over the other ones. I assume I must be doing something wrong due changes in the newer version, here are the relevant code in question:
#Configuration
#LoadBalancerClients(defaultConfiguration = ClientConfiguration.class)
public class WebClientConfiguration {
#Bean
#Qualifier("microserviceWebClient")
#ConditionalOnMissingBean(name = "microserviceWebClient")
public WebClient microserviceWebClient(#Qualifier("microserviceWebClientBuilder") WebClient.Builder builder) {
return builder.build();
}
#Bean
#Qualifier("microserviceWebClientBuilder")
#ConditionalOnMissingBean(name = "microserviceWebClientBuilder")
#LoadBalanced
public WebClient.Builder microserviceWebClientBuilder() {
return WebClient.builder();
}
#Bean
#Primary
public ReactorLoadBalancerExchangeFilterFunction reactorLoadBalancerExchangeFilterFunction(
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
//the transformer is currently null, there wasn't a transformer before the upgrade
return new CustomExchangeFilterFunction(loadBalancerFactory, transformer);
}
}
There are also some Feign Client related configs here which I will omit, since it's not (or shouldn't be) playing a role in this problem:
public class ClientConfiguration {
/**
* The property key within the feign clients configuration context for the feign client name.
*/
public static final String FEIGN_CLIENT_NAME_PROPERTY = "feign.client.name";
public ClientConfiguration() {
}
//Creates a new BiPredicate for shouldClose. This will be used to determine if HTTP Connections should be automatically closed or not.
#Bean
#ConditionalOnMissingBean
public BiPredicate<Response, Type> shouldClose() {
return (Response response, Type type) -> {
if(type instanceof Class) {
Class<?> currentClass = (Class<?>) type;
return (null == AnnotationUtils.getAnnotation(currentClass, EnableResponseStream.class));
}
return true;
};
}
//Creates a Custom Decoder
#Bean
public Decoder createCustomDecoder(
ObjectFactory<HttpMessageConverters> converters, BiPredicate<Response, Type> shouldClose
) {
return new CustomDecoder(converters, shouldClose);
}
#Bean
#Qualifier("loadBalancerName")
public String loadBalancerName(PropertyResolver propertyResolver) {
String name = propertyResolver.getProperty(FEIGN_CLIENT_NAME_PROPERTY);
if(StringUtils.hasText(name)) {
// we are in a feign context
return name;
}
// we are in a LoadBalancerClientFactory context
name = propertyResolver.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
Assert.notNull(name, "Could not find a load balancer name within the configuration context!");
return name;
}
#Bean
public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(
BeanFactory beanFactory, #Qualifier("loadBalancerName") String loadBalancerName
) {
return new CustomRoundRobinLoadBalancer(
beanFactory.getBeanProvider(ServiceInstanceListSupplier.class),
loadBalancerName
);
}
#Bean
#Primary
public ServiceInstanceListSupplier serviceInstanceListSupplier(
#Qualifier(
"filter"
) Predicate<ServiceInstance> filter, DiscoveryClient discoveryClient, Environment environment, #Qualifier(
"loadBalancerName"
) String loadBalancerName
) {
// add service name to environment if necessary
if(environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME) == null) {
StandardEnvironment wrapped = new StandardEnvironment();
if(environment instanceof ConfigurableEnvironment) {
((ConfigurableEnvironment) environment).getPropertySources()
.forEach(s -> wrapped.getPropertySources().addLast(s));
}
Map<String, Object> additionalProperties = new HashMap<>();
additionalProperties.put(LoadBalancerClientFactory.PROPERTY_NAME, loadBalancerName);
wrapped.getPropertySources().addLast(new MapPropertySource(loadBalancerName, additionalProperties));
environment = wrapped;
}
return new FilteringInstanceListSupplier(filter, discoveryClient, environment);
}
}
There was a change in the ExchangeFilter constructor, but as far as I can tell, it accepts that empty transformer,I don't know if it's supposed to:
public class CustomExchangeFilterFunction extends ReactorLoadBalancerExchangeFilterFunction {
private static final ThreadLocal<ClientRequest> REQUEST_HOLDER = new ThreadLocal<>();
//I think it's wrong but I don't know what to do here
private static List<LoadBalancerClientRequestTransformer> transformersList;
private final Factory<ServiceInstance> loadBalancerFactory;
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory) {
this(loadBalancerFactory);
///according to docs, but I don't know where and if I need to use this
#Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
#Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header(instance.getInstanceId())
.build();
}
};
}
public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory, List<LoadBalancerClientRequestTransformer> transformersList) {
super(loadBalancerFactory, transformersList); //the changed constructor
this.loadBalancerFactory = loadBalancerFactory;;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
// put the current request into the thread context - ugly, but couldn't find a better way to access the request within
// the choose method without reimplementing nearly everything
REQUEST_HOLDER.set(request);
try {
return super.filter(request, next);
} finally {
REQUEST_HOLDER.remove();
}
}
//used to be an override, but the function has changed
//code execution doesn't even get this far yet
protected Mono<Response<ServiceInstance>> choose(String serviceId) {
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
if(loadBalancer == null) {
return Mono.just(new EmptyResponse());
}
ClientRequest request = REQUEST_HOLDER.get();
// this might be null, if the underlying implementation changed and this method is no longer executed in the same
// thread
// as the filter method
Assert.notNull(request, "request must not be null, underlying implementation seems to have changed");
return choose(loadBalancer, filter);
}
protected Mono<Response<ServiceInstance>> choose(
ReactiveLoadBalancer<ServiceInstance> loadBalancer,
Predicate<ServiceInstance> filter
) {
return Mono.from(loadBalancer.choose(new DefaultRequest<>(filter)));
}
}
There were pretty big changes in the CustomExchangeFilterFunction, but the current execution doesn't even get there. It fails here, in .getIfAvailable(...):
public class CustomRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final int DEFAULT_SEED_POSITION = 1000;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
private final int seedPosition;
private final AtomicInteger position;
private final Map<String, AtomicInteger> positionsForVersions = new HashMap<>();
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId
) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(DEFAULT_SEED_POSITION));
}
public CustomRoundRobinLoadBalancer (
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
int seedPosition
) {
Assert.notNull(serviceInstanceListSupplierProvider, "serviceInstanceListSupplierProvider must not be null");
Assert.notNull(serviceId, "serviceId must not be null");
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.serviceId = serviceId;
this.seedPosition = seedPosition;
this.position = new AtomicInteger(seedPosition);
}
#Override
// we have no choice but to use the raw type Request here, because this method overrides another one with this signature
public Mono<Response<ServiceInstance>> choose(#SuppressWarnings("rawtypes") Request request) {
//fails here!
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map((List<ServiceInstance> instances) -> getInstanceResponse(instances, request));
}
}
Edit: after some deeper stacktracing, it seems that it does go into the CustomFilterFunction and invokes the constructor with super(loadBalancerFactory, transformer)
I found the problem or a workaround. I was using #LoadBalancerClients because I thought it would just set the same config for all clients that way (even if I technically only have one atm). I changed it to ##LoadBalancerClient and it suddenly worked. I don't quite understand why this made a difference but it did!

How Do I Customize Spring Error Timestamps Without Causing Exceptions?

When there are errors in my backend, SpringBoot returns a json result with the errors.
I wanted to change the timestamp in the json to be readable so I customised the attributes:
#Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
private static final String TIMESTAMP = "timestamp";
#Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
//Let Spring handle the error first
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
//Format & update timestamp
Object timestamp = errorAttributes.get(TIMESTAMP);
if(timestamp == null) {
errorAttributes.put(TIMESTAMP, dateFormat.format(new Date()));
} else {
errorAttributes.put(TIMESTAMP, dateFormat.format((Date)timestamp));
}
return errorAttributes;
}
}
However, now when there is a 404, an Exception is thrown because of this customisation:
E 23:21:36 88 [dispatcherServlet].log - Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Date
at org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$StaticView.render(ErrorMvcAutoConfiguration.java:224) ~[spring-boot-autoconfigure-2.1.4.RELEASE.jar:2.1.4.RELEASE]
The ErrorMvcAutoConfiguration is trying to parse the timestamp attribute:
#Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
StringBuilder builder = new StringBuilder();
Date timestamp = (Date) model.get("timestamp");
How can I customize the timestamp in the error response without affecting other standard error processing?
It looks like you're working with an outdated version of spring mvc.
And we're talking about ErrorMvcAutoConfiguration.java
There was a bug:
See here
It was fixed in a commit d1d953819ac9f0c0ece5160b96899030cabda46c on Sep 12, 2020 into spring boot repository by Phil Webb into version 2.2.x and it's also been forward merged to 2.3.x and 2.4.x.
So now it looks like:
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Log logger = LogFactory.getLog(StaticView.class);
#Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
// !!!note this line!!!!
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
// !!! and this line !!!
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
Having said that, usually people customize the error page (from my experience at least) and do not even use this default view.
There are plenty of tutorials about that, here is one of them

Could not write JSON: JsonObject; nested exception is com.fasterxml.jackson.databind.JsonMappingException: JsonObject

Spring boot 2.5
#PostMapping("/cart/product")
public Response addProduct(#RequestBody Map<String, Object> payloadMap) {
logger.info("addProduct: payloadMap: " + payloadMap);
String userName = payloadMap.get("user_name").toString();
final Product product = new ObjectMapper().convertValue(payloadMap.get("product"), Product.class);
int quantity = (int) payloadMap.get("quantity");
Cart findCart = cartRepository.findByUsername(userName);
if (findCart == null) {
Cart cart = new Cart();
cart.setCreated(new Date());
cart.setUsername(userName);
cart.addProduct(product, quantity);
cartRepository.save(cart);
logger.info("addProduct: success_add_product_to_new_cart: " + cart);
return ResponseService.getSuccessResponse(GsonUtil.gson.toJsonTree(cart));
} else {
findCart.addProduct(product, quantity);
logger.info("addProduct: before_save_exist_cart: " + findCart);
cartRepository.save(findCart);
logger.info("addProduct: success_add_product_to_exist_cart: " + findCart);
return ResponseService.getSuccessResponse(GsonUtil.gson.toJsonTree(findCart));
}
}
public class ResponseService {
private static final int SUCCESS_CODE = 0;
private static final String SUCCESS_MESSAGE = "Success";
private static final int ERROR_CODE = -1;
private static Logger logger = LogManager.getLogger(ResponseService.class);
public static Response getSuccessResponse(JsonElement body) {
Response response = new Response(SUCCESS_CODE, SUCCESS_MESSAGE, body);
logger.info("getSuccessResponse: response = " + response);
return response;
}
import com.google.gson.JsonElement;
public class Response {
private int code;
private String message;
private JsonElement body;
public Response(int code, String message) {
this.code = code;
this.message = message;
}
public Response(int code, String message, JsonElement body) {
this.code = code;
this.message = message;
this.body = body;
}
But I get error when try to return response:
2020-04-12 12:02:18.825 INFO 9584 --- [nio-8092-exec-1] r.o.s.e.controllers.CartController : addProduct: success_add_product_to_new_cart: Cart{id=130, username='admin#admin.com', created=Sun Apr 12 12:02:18 EEST 2020, updated=null, productEntities=[
ProductEntity{id=131, created=Sun Apr 12 12:02:18 EEST 2020, updated=null, quantity=1, orders=null, product=
Product{id=132, name='product name', description='product description', created=Tue Mar 10 22:34:15 EET 2020, updated=null, price=11.15, currency='EUR', images=[http://www.gravatar.com/avatar/44444?s=200x200&d=identicon, http://www.gravatar.com/avatar/33333?s=200x200&d=identicon]}}], totalAmount=11.15, currency='EUR'}
2020-04-12 12:02:18.836 INFO 9584 --- [nio-8092-exec-1] r.o.s.e.service.ResponseService : getSuccessResponse: response = Response{code = 0, message = 'Success', body = '{"id":130,"username":"admin#admin.com","created":"Apr 12, 2020, 12:02:18 PM","productEntities":[{"id":131,"created":"Apr 12, 2020, 12:02:18 PM","quantity":1,"product":{"id":132,"name":"product name","description":"product description","created":"Mar 10, 2020, 10:34:15 PM","price":11.15,"currency":"EUR","images":["http://www.gravatar.com/avatar/44444?s=200x200&d=identicon","http://www.gravatar.com/avatar/33333?s=200x200&d=identicon"]}}],"totalAmount":11.15,"currency":"EUR"}'}
2020-04-12 12:02:18.861 WARN 9584 --- [nio-8092-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: JsonObject; nested exception is com.fasterxml.jackson.databind.JsonMappingException: JsonObject (through reference chain: ru.otus.software_architect.eshop_orders.api.Response["body"]->com.google.gson.JsonObject["asBoolean"])]
Spring Boot uses jackson by default to serialize json. In Response object you have field JsonElement body. It is object from gson package and jackson don't know how to serialize that.
Solution: add property (in the application.properties file) to use gson instead of jackson. Note that Spring Boot version is important.
Spring Boot >= 2.3.0.RELEASE:
spring.mvc.converters.preferred-json-mapper=gson
Spring Boot < 2.3.0.RELEASE:
spring.http.converters.preferred-json-mapper=gson
More informations:
Configuring Spring Boot to use Gson instead of Jackson
Spring Boot 2.3 Release Notes
Application.properties
spring.mvc.converters.preferred-json-mapper=gson
I've found a workaround by keeping Jackson but implementing my own Serializer for the classes that cause Jackson serialisation issue.
It's hackish solution but it's working now.
public class GSONObjectSerializer extends SimpleSerializers {
private static final long serialVersionUID = -8745250727467996655L;
private class EmptySerializer extends StdSerializer<Object> {
/**
*
*/
private static final long serialVersionUID = 5435498165882848947L;
protected EmptySerializer(Class t) {
super(t);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// --> Will write an empty JSON string
gen.writeStartObject();
gen.writeEndObject();
}
}
#Override
public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
// --> Here is to setup which class will be serialised by the custom serializer.
if (type.isTypeOrSubTypeOf(JsonObject.class) || type.isTypeOrSubTypeOf(StripeResponse.class)) {
return new EmptySerializer(type.getRawClass());
}
return super.findSerializer(config, type, beanDesc);
}
}
And you can register your serializer like this
#Configuration
public class SerialisationConfig {
#Bean
public ObjectMapper createMapper() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.setSerializers(new GSONObjectSerializer());
return Jackson2ObjectMapperBuilder.json().modules(Arrays.asList(simpleModule)).build();
}
}

Spring Cloud OpenFeign Failed to Create Dynamic Query Parameters

Spring cloud openFeign can't create dynamic query parameters. It throws below exception because SpringMvcContract tries to find the RequestParam value attribute which doesn't exist.
java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0
#RequestMapping(method = RequestMethod.GET, value = "/orders")
Pageable<Order> searchOrder2(#RequestParam CustomObject customObject);
I tried using #QueryMap instead of #RequestParam but #QueryMap does not generate query parameters.
Btw #RequestParam Map<String, Object> params method parameter works fine to generate a dynamic query parameter.
But I want to use a custom object in which the feign client can generate dynamic query parameters from the object's attributes.
From Spring Cloud OpenFeign Docs:
Spring Cloud OpenFeign provides an equivalent #SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map
So your code should be:
#RequestMapping(method = RequestMethod.GET, value = "/orders")
Pageable<Order> searchOrder2(#SpringQueryMap #ModelAttribute CustomObject customObject);
spring-cloud-starter-feign has a open issue for supporting pojo object as request parameter. Therefore I used a request interceptor that take object from feign method and create query part of url from its fields. Thanks to #charlesvhe
public class DynamicQueryRequestInterceptor implements RequestInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicQueryRequestInterceptor.class);
private static final String EMPTY = "";
#Autowired
private ObjectMapper objectMapper;
#Override
public void apply(RequestTemplate template) {
if ("GET".equals(template.method()) && Objects.nonNull(template.body())) {
try {
JsonNode jsonNode = objectMapper.readTree(template.body());
template.body(null);
Map<String, Collection<String>> queries = new HashMap<>();
buildQuery(jsonNode, EMPTY, queries);
template.queries(queries);
} catch (IOException e) {
LOGGER.error("IOException occurred while try to create http query");
}
}
}
private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
if (!jsonNode.isContainerNode()) {
if (jsonNode.isNull()) {
return;
}
Collection<String> values = queries.computeIfAbsent(path, k -> new ArrayList<>());
values.add(jsonNode.asText());
return;
}
if (jsonNode.isArray()) {
Iterator<JsonNode> it = jsonNode.elements();
while (it.hasNext()) {
buildQuery(it.next(), path, queries);
}
} else {
Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
if (StringUtils.hasText(path)) {
buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
} else {
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}

Spring-boot MultipartFile issue with ByteArrayResource

I'm trying to implement a rest api consuming excel file. I'm using spring-boot and code is available here.
Code works fine when using FileSystemResource for payload. But i'm not able to make the code work with ByteArrayResource in replacement of FileSystemResource:
RestApi.java:
#RestController
public class RestApi {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
#PostMapping("/api/upload")
public ResponseEntity<?> uploadFile(#RequestParam("file") MultipartFile uploadfile) {
LOGGER.debug("Single file upload!");
try {
LOGGER.info("\n\n ****** File name: {}, type {}! ************", uploadfile.getOriginalFilename(), uploadfile.getContentType());
this.processExcelFile(uploadfile.getInputStream());
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>("Successfully uploaded - " + uploadfile.getOriginalFilename(), new HttpHeaders(), HttpStatus.OK);
}
private List<String> processExcelFile(InputStream stream) throws Exception {
List<String> result = new ArrayList<String>();
//Create Workbook instance holding reference to .xlsx file
try(XSSFWorkbook workbook = new XSSFWorkbook(stream);) {
//Get first/desired sheet from the workbook
XSSFSheet sheet = workbook.getSheetAt(0);
//Iterate through each rows one by one
Iterator<Row> rowIterator = sheet.iterator();
while (rowIterator.hasNext()) {
Row row = rowIterator.next();
String cellValue = row.getCell(0).getRichStringCellValue().toString();
result.add(cellValue);
LOGGER.info("\n\n ****** Cell value: {} ************", cellValue);
}
return result;
}
}
}
RestApiTest:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class RestApiTest {
#Autowired
private TestRestTemplate restTemplate;
#Autowired
private ResourceLoader loader;
#Test
public void testUploadFile() throws Exception {
Resource resource = this.loader.getResource("classpath:test.xlsx");
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
// parts.add("file", new FileSystemResource(resource.getFile()));
parts.add("file", new ByteArrayResource(IOUtils.toByteArray(resource.getInputStream())));
String response = this.restTemplate.postForObject("/api/upload", parts, String.class);
Assertions.assertThat(response).containsIgnoringCase("success");
}
}
I'm getting following error when running test:
java.lang.AssertionError:
Expecting:
<"{"timestamp":1487852597527,"status":400,"error":"Bad Request","exception":"org.springframework.web.multipart.support.MissingServletRequestPartException","message":"Required request part 'file' is not present","path":"/api/upload"}">
to contain:
<"success">
(ignoring case)
Any idea?
when using loader.getResource(...) you must use resource itself as answered above. So you don't need ByteArrayResource. I got this problem, but I'm not using resource from classpath. So if someone really need to use ByteArrayResource, here is my workaround
public class FileNameAwareByteArrayResource extends ByteArrayResource {
private String fileName;
public FileNameAwareByteArrayResource(String fileName, byte[] byteArray, String description) {
super(byteArray, description);
this.fileName = fileName;
}
#Override
public String getFilename() {
return fileName;
}
}
and then use it
parts.add("file", new FileNameAwareByteArrayResource("filename", byteArray));

Resources