Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate and Thymeleaf infinite recursion

I have 2 classes with one atribute mapped with OneToMany/ManyToOne relation. The problem is that when i do the select and pass to te view, i want to parse the object to javascript with Thymeleaf, but it loop infinite cause of the relation. My to classes: Class Player:

@Entity
@Table(name = "player")
public class Player {

@Id
@Column(name = "id")
@GeneratedValue
private int id;

@Column(name = "level")
private int level;

@Column(name = "experience")
private int experience;

@OneToMany(mappedBy="player", cascade = CascadeType.ALL)
private List<InventoryItem> inventory;

// Constructor
public Player() {
}

// Getters & Setters...
}

Class InventoryItem:

@Entity
@Table(name = "inventory_item")
public class InventoryItem {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @ManyToOne
    @JoinColumn(name="id_player")
    private Player player;


    public InventoryItem() {
    }

    //Getters and Setters...
}

Then i pass the Player object to the view and console.log it with javascript:

<script th:inline="javascript">
/*<![CDATA[*/
    console.log([[${player}]]);
/*]]>*/
</script>

And this is the error: enter image description here

How can i prevent the bi-directional relation when parsing to javascript, something like ignore the Player atrubute from all the InventoryItems?

like image 492
Raxkin Avatar asked Apr 27 '15 12:04

Raxkin


1 Answers

I faced this problem today and I guess Thymeleaf does not offer an easy solution for that. You can always set the references to the parent to null before passing it to Thymeleaf, but that seems kind of ugly.

Investigating Thymeleaf's source code, I noticed that it uses Introspector to get information about the properties, so we can hide some properties by implementing a BeanInfo. The easiest way to do that is to create a class on the same package of your bean, with BeanInfo appended to the name. You can extend SimpleBeanInfo and implement only the methods that interest you (in this case getPropertyDescriptors).

In your example, you could do:

public class InventoryItemBeanInfo extends SimpleBeanInfo {
    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        try {  
            PropertyDescriptor id = new PropertyDescriptor("id", InventoryItem.class);         
            PropertyDescriptor[] descriptors = {id};
            return descriptors;
        } catch (IntrospectionException e) {
            throw new Error(e.toString());
        }
    }
}

This way, Thymeleaf wouldn't see your Player property and you wouldn't have the infinite recursion anymore.

The downside of this solution is that you have to remember that everytime you change the attributes of your InventoryItem bean you have to go to your BeanInfo and add the new attribute to it.

So I came up with another solution. It's a little bit more complicated, but once it's set up it's easier to maintain.

We start by creating an annotation to indicate that the property should be hidden. Let's call it HiddenProperty:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HiddenProperty {}

Now we implement a generic BeanInfo class that will inspect a bean using reflection and find the properties. We will also check for the existence of our annotation and when it's present we ignore the method:

public class HiddenPropertyAwareBeanInfo extends SimpleBeanInfo {
    private Class<?> beanClass;
    private PropertyDescriptor[] descriptors;

    public HiddenPropertyAwareBeanInfo(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        if(descriptors != null) {
            return descriptors;
        }

        Method[] methodList = beanClass.getMethods();
        List<PropertyDescriptor> propDescriptors = new ArrayList<>();

        for(Method m : methodList) {
            if(Modifier.isStatic(m.getModifiers())) {
                continue;
            }

            try {
                if(m.getParameterCount() == 0 && !m.isAnnotationPresent(HiddenProperty.class)) {
                    if(m.getName().startsWith("get")) {
                        propDescriptors.add(new PropertyDescriptor(m.getName().substring(3), beanClass));
                    } else if(m.getName().startsWith("is")) {
                        propDescriptors.add(new PropertyDescriptor(m.getName().substring(2), beanClass));
                    }
                }
            } catch(IntrospectionException ex) {
                continue;
            }
        }

        descriptors = new PropertyDescriptor[propDescriptors.size()];
        return propDescriptors.toArray(descriptors);
    }
}

Now, create your BeanInfo that extends this class:

public class InventoryItemBeanInfo extends HiddenPropertyAwareBeanInfo {
    public InventoryItemBeanInfo() {
        super(InventoryItem.class);
    }
}

Finally, annotate the getters of the properties you want to hide:

@Entity
@Table(name = "inventory_item")
public class InventoryItem {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private int id;

    @ManyToOne
    @JoinColumn(name="id_player")
    private Player player;


    public InventoryItem() {
    }

    @HiddenProperty
    public Player getPlayer() {
        return this.player;
    }
    //Other getters and setters...
}

And there you have it. Now you can change your properties as much as you want and your Player property will not be seen by Thymeleaf.

One important thing to say about this solution is that it is not 100% compliant with the Java Beans spec, it doesn't consider the existence of indexed properties and some other details. You can take a look at the Introspector's source code to see how they extract this information and improve this solution.

But, personally, I think this should be fixed inside Thymeleaf. Thymeleaf is an incredibly elegant tool and having to resort to that kind of workaround makes me kind of sad.

like image 197
Gui Meira Avatar answered Oct 18 '22 02:10

Gui Meira