Issue
I have a problem using a custom type into my JPA entity and then convert and store it into the database as JSON string.
Here is the base JPA entity: Discount (We are using Lombok and builders for creating object instances. The issue occurs on the two fields of type LocalizedTexts):
@Value
@NoArgsConstructor(force = true)
@AllArgsConstructor
@Builder(builderMethodName = "internalBuilder")
@Entity
@Table(name="discount")
public class Discount {
@Id
@Column(name = "ID", nullable = false, unique = true)
@Type(type="uuid-char")
UUID id;
@Column(name="NO", nullable = false, unique = true)
String no;
@Column(name="I_NO", nullable = false, unique = true)
Integer iNo;
@Convert(converter = JpaJsonConverter.class)
@Column(name="DESIGNATION", nullable = false)
LocalizedTexts designation;
@Convert(converter = JpaJsonConverter.class)
@Column(name="PRINT_TEXT")
LocalizedTexts printText;
...
/**
* Builder with required parameters
*/
public static Discount.DiscountBuilder builder(UUID id, String no, LocalizedTexts designation) {
return internalBuilder()
.id(id)
.no(no)
.designation(designation);
}
// Getters
...
}
I am using an Attribute converter to transform the custom type (extending a Map<Object, String):
@Converter
public class JpaJsonConverter implements AttributeConverter<Object, String> {
private final static ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(Object localizedTexts) {
String localizedTextsJson = null;
try {
localizedTextsJson = objectMapper.writeValueAsString(localizedTexts);
} catch (JsonProcessingException ex) {
//throw new RuntimeException();
}
return localizedTextsJson;
}
@Override
public Object convertToEntityAttribute(String localizedTextsJSON) {
Object localizedTexts = null;
try {
localizedTexts = objectMapper.readValue(localizedTextsJSON, Object.class);
} catch (IOException ex) {
//throw new RuntimeException();
}
return localizedTexts;
}
}
And here is the custom type: LocalizedTexts (Map<Enum,String>):
public class LocalizedTexts extends HashMap<Language, String> implements EntityBase {
public LocalizedTexts() {
}
public LocalizedTexts(Map map) {
this.putAll(map);
}
public static LocalizedTextsBuilder internalBuilder() {
return new LocalizedTextsBuilder();
}
public static class LocalizedTextsBuilder {
LocalizedTextsBuilder() {
}
public LocalizedTexts build() {
return new LocalizedTexts();
}
public String toString() {
return "LocalizedTexts.LocalizedTextsBuilder()";
}
}
}
The issue occurs when I am trying to save a Discount object into the database:
...
discountRepository.save(discount);
...
I have checked and the attribute converter works as expected (transform LocalizedTexts into JSON strings). The full stack trace of the exception I get is the following:
org.springframework.orm.jpa.JpaSystemException: Could not set field value [{de=Rabatt 1, en=Discount 1}] value by reflection : [class net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation] setter of net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation; nested exception is org.hibernate.PropertyAccessException: Could not set field value [{de=Rabatt 1, en=Discount 1}] value by reflection : [class net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation] setter of net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:331)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at jdk.proxy2/jdk.proxy2.$Proxy116.save(Unknown Source)
at net.xxxxx.yyyyy.svc_voucher.messagehandler.DiscountHandler.handleCommand(DiscountHandler.java:58)
at net.xxxxx.yyyyy.svc_voucher.handlers.TestDiscountHandler.Sending_script_to_messageHandler_handleCommand(TestDiscountHandler.java:109)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: org.hibernate.PropertyAccessException: Could not set field value [{de=Rabatt 1, en=Discount 1}] value by reflection : [class net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation] setter of net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation
at org.hibernate.property.access.spi.SetterFieldImpl.set(SetterFieldImpl.java:72)
at org.hibernate.tuple.entity.AbstractEntityTuplizer.setPropertyValues(AbstractEntityTuplizer.java:681)
at org.hibernate.tuple.entity.PojoEntityTuplizer.setPropertyValues(PojoEntityTuplizer.java:144)
at org.hibernate.persister.entity.AbstractEntityPersister.setPropertyValues(AbstractEntityPersister.java:5252)
at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:498)
at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:241)
at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:318)
at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:172)
at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:70)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:829)
at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:816)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311)
at jdk.proxy2/jdk.proxy2.$Proxy113.merge(Unknown Source)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:650)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:639)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
... 42 more
Caused by: java.lang.IllegalArgumentException: Can not set final net.xxxxx.yyyyy.entity.internal.LocalizedTexts field net.xxxxx.yyyyy.svc_voucher.entity.Discount.designation to java.util.LinkedHashMap
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:83)
at java.base/java.lang.reflect.Field.set(Field.java:799)
at org.hibernate.property.access.spi.SetterFieldImpl.set(SetterFieldImpl.java:52)
... 81 more
Solution
At first it's tempting to believe that the final
modifier is the cause of the issue, but in fact it's not.
Let's try this simple test:
import java.lang.reflect.Field;
class Discount
{
private final Integer id = 0;
}
public class Test
{
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException
{
Discount d = new Discount();
Field f = Discount.class.getDeclaredField("id");
f.set(d, 1);
}
}
We get:
java.lang.IllegalAccessException: class Test cannot access a member of class Discount with modifiers "private final"
This is not the error that you are getting (your stacktrace shows an IllegalArgumentException
, not an IllegalAccessException
).
Let's change the code to add a setAccessible(true)
:
import java.lang.reflect.Field;
class Discount
{
private final Integer id = 0;
}
public class Test
{
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException
{
Discount d = new Discount();
Field f = Discount.class.getDeclaredField("id");
f.setAccessible(true);
f.set(d, 1);
}
}
This time it works, no exception is thrown.
Now let's replace the 1
with "1"
:
import java.lang.reflect.Field;
class Discount
{
private final Integer id = 0;
}
public class Test
{
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException
{
Discount d = new Discount();
Field f = Discount.class.getDeclaredField("id");
f.setAccessible(true);
f.set(d, "1");
}
}
We get:
java.lang.IllegalArgumentException: Can not set final java.lang.Integer field Discount.id to java.lang.String
This time this is exactly the same error as yours. The IllegalArgumentException
means that we're trying to assign an incompatible value (here, a String
to an Integer
).
Your stacktrace says that you're trying to assign a LinkedHashMap
to the designation
field, which is incompatible indeed (designation
being a LocalizedTexts
).
To understand where the LinkedHashMap
comes from, let's take a look at your JpaJsonConverter
. The method responsible for converting a JSON string to a LocalizedTexts
object is:
public Object convertToEntityAttribute(String localizedTextsJSON) {
Object localizedTexts = null;
try {
localizedTexts = objectMapper.readValue(localizedTextsJSON, Object.class);
} catch (IOException ex) {
//throw new RuntimeException();
}
return localizedTexts;
}
You're calling readValue()
with a JSON string containing an object, and Object.class
as second argument.
In such a case, Jackson returns a LinkedHashMap
. It has no way to know that you want a LocalizedTexts
.
Here is a possible way to fix the method:
@SuppressWarnings("unchecked")
public Object convertToEntityAttribute(String localizedTextsJSON) {
LocalizedTexts localizedTexts = new LocalizedTexts();
try {
Map<String,String> map = (Map<String,String>)objectMapper.readValue(localizedTextsJSON, Object.class);
for(Map.Entry<String,String> e : map.entrySet())
localizedTexts.put(Language.valueOf(e.getKey()), e.getValue());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return localizedTexts;
}
Answered By - Olivier
Answer Checked By - Willingham (JavaFixing Volunteer)