10

I have encountered a very strange exception, and I don't know how to find the reason.

Business background: Add goods and meantime it's price list, a goods have 5 price for diff level user.

In controller, first convert goodForm to goods by using dozer, then call goodsService to save goods. In goodsService after saving goods, traversal goods price list and populate goodsId to goods price,

GoodsForm:
@Mapping("priceList")
List<GoodsPriceForm> goodsPriceFormList;
Goods:
List<GoodsPrice> priceList;

Controller: 
Goods goods = BeanMapper.map(goodsForm, Goods.class);
goodsService.saveGoods(adminId, goods);

GoodsService:
goodsDao.save(goods);
goods.getPriceList().forEach(p -> p.setGoodsId(goods.getId()));
goodsPriceDao.save(goods.getPriceList());

But it throw exception:

2015-11-27 17:10:57,042 [http-nio-8081-exec-8] ERROR o.a.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: com.foo.goods.model.GoodsPrice cannot be cast to com.foo.goods.model.GoodsPrice] with root cause
java.lang.ClassCastException: com.foo.goods.model.GoodsPrice cannot be cast to com.foo.goods.model.GoodsPrice
at com.foo.goods.service.GoodsService$$Lambda$11/310447431.accept(Unknown Source) ~[na:na]
at java.util.ArrayList.forEach(ArrayList.java:1249) ~[na:1.8.0_51]
at com.foo.goods.service.GoodsService.saveGoods(GoodsService.java:34) ~[classes/:na]

This error message let me feel very confused. In addition I write a unit test wanted to repeat this, but failed.

GoodsForm form = new GoodsForm();
form.setGoodsPriceFormList(Lists.newArrayList(new GoodsPriceForm((byte) 1, BigDecimal.valueOf(10)),
new GoodsPriceForm((byte) 2, BigDecimal.valueOf(9)),
new GoodsPriceForm((byte) 3, BigDecimal.valueOf(8))));

Goods goods = BeanMapper.map(form, Goods.class);
goods.getPriceList().forEach(p -> p.setGoodsId(goods.getId()));

Run this unit test, it executed ok. So why in real web situation(Spring boot + Jpa) it's failed, but in unit test situation it's ok?


Controller:
System.out.println("PriceList: " + goods.getPriceList().getClass().getClassLoader());//PriceList: null
System.out.println(goods.getPriceList().get(0).getClass().getClassLoader()); //java.lang.ClassCastException: com.foo.goods.model.GoodsPrice cannot be cast to com.foo.goods.model.GoodsPrice

If I generated a packaged jar, then execute this jar

java -jar target/myapp.jar

In this case without above exception.


And I commented spring-boot-devtools in pom.xml, then started application, without above exception.

zhuguowei
  • 8,401
  • 16
  • 70
  • 106
  • 10
    The only time I have had an exception like that is if you load the same class with 2 different class loaders. Can you try printing out the class loader of each object? – Wim Deblauwe Nov 27 '15 at 10:48
  • 5
    Then the same class was loaded by two different class loaders. First measure is to have the class in just one jar at one location. – Joop Eggen Nov 27 '15 at 10:48
  • @Wim Deblauwe I have tried your way, please see my supplemental content at bottom of this post – zhuguowei Nov 27 '15 at 11:08
  • 2
    So the goods.getPriceList already contains the wrong typed objects. Which might be the case (type erasure) with bean manipulating tools (**dozer**?). They maybe use another jar/class with same name. Dozer might be used be the web server, and have an other ClassLoader. BTW `BigDecimal.valueOf("9.00")` might be better: the precision then is 2 digits. `getClass().getProtectionDomain().getCodeSource().getLocation().toString()` could tell the jar, when you would not get that error. – Joop Eggen Nov 27 '15 at 11:17
  • @Joop Eggen In debug mode, I successed output the classloader, goodsPrice: sun.misc.Launcher$AppClassLoader@14dad5dc, goods: org.springframework.boot.devtools.restart.classloader.RestartClassLoader@591c6338 – zhuguowei Nov 27 '15 at 12:16
  • 1
    No static price list? No price list kept longer than the application's life? Better loaded at application's start. – Joop Eggen Nov 27 '15 at 12:22
  • Possible duplicate of [can't cast to implemented interface](http://stackoverflow.com/questions/8035877/cant-cast-to-implemented-interface) – Tobias Liefke Nov 27 '15 at 21:39

2 Answers2

13

By default, any open project in your IDE will be loaded using the “restart” classloader, and any regular .jar file will be loaded using the “base” classloader. If you work on a multi-module project, and not each module is imported into your IDE, you may need to customize things. To do this you can create a META-INF/spring-devtools.properties file.

The spring-devtools.properties file can contain restart.exclude. and restart.include. prefixed properties. The include elements are items that should be pulled-up into the “restart” classloader, and the exclude elements are items that should be pushed down into the “base” classloader. The value of the property is a regex pattern that will be applied to the classpath.

My Solution: put META-INF/spring-devtools.properties inside resources folder, and add this content

restart.include.dozer=/dozer-5.5.1.jar

Please see : http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-devtools-customizing-classload

zhuguowei
  • 8,401
  • 16
  • 70
  • 106
  • 3
    If you use dozer-spring, add this too : restart.include.dozer-spring=/dozer-spring-5.5.1.jar – cLyric May 12 '16 at 16:20
  • 3
    This file also supports regexp paths, so I'd rather use `restart.include.dozer=/dozer-[\\w\\d.]+\\.jar` instead. It's easy to forget to update this property when updating dependency. – Michał Stochmal Sep 19 '19 at 14:18
1

You are using two different ClassLoader here. An identical Class loaded with two different ClassLoader is considered as two different Class by the JVM.

The solution to fix this is simple : Use an Interface.

Interfaces are able to abstract this problem, and you can interchange the object they implement between ClassLoaders without limitation, as long as you don't reference the implementation directly.

Guillaume F.
  • 5,905
  • 2
  • 31
  • 59
  • 1
    I go with you on the first part, but your second part is totally wrong. An interface is, from the view of a class loader, a class file as well. So `classLoader1.loadClass("...MyInterface") != classLoader2.loadClass("...MyInterface")`. – Tobias Liefke Nov 27 '15 at 20:48
  • Oh you are right, I didn't mean it that way, but it totally came out that way. I just edited my wording to make it clearer. – Guillaume F. Nov 27 '15 at 21:12
  • 1
    I think this is still vague. If you read my duplication mark on the OP you will see, that this was about an interface. As long as you still load the interface with the two different classloaders, you will have no benefit from using an interface. I know that there are situations where an interface or superclass from the _common parent class loader_ will help, but I don't think that the current question belongs to one of that situations. You can still prove me wrong with an example... – Tobias Liefke Nov 27 '15 at 22:04