I am building an application using JPA, JSF, EJB, Derby. At this point the application is still small. I have a form in the application to add new products. When adding data to the db it goes smoothly until I restart the application or the server. When I restart either the server or app I get java.lang.StackOverflowError
, I still can query the db for the data represented by the product db, but creation product is not possible. I have only 5 entries in the db, as of now, but I am concerned about this happening so early.
This is the Ejb (Getter, setter and constructors removed for simplicity):
@Stateless
public class ProductEJB{
@PersistenceContext(unitName = "luavipuPU")
private EntityManager em;
public List<Product> findAllProducts()
{
TypedQuery<Product> query = em.createNamedQuery("findAllProducts", Product.class);
return query.getResultList();
}
public Product findProductById(int productId)
{
return em.find(Product.class, productId);
}
public Product createProduct(Product product)
{
product.setDateAdded(productCreationDate());
em.persist(product);
return product;
}
public void updateProduct(Product product)
{
em.merge(product);
}
public void deleteProduct(Product product)
{
product = em.find(Product.class, product.getProduct_id());
em.remove(em.merge(product));
}
this is the ProductController (Getter, setter and constructors removed for simplicity):
@Named
@RequestScoped
public class ProductController {
@EJB
private ProductEJB productEjb;
@EJB
private CategoryEJB categoryEjb;
private Product product = new Product();
private List<Product> productList = new ArrayList<Product>();
private Category category;
private List<Category> categoryList = new ArrayList<Category>();
public String doCreateProduct()
{
product = productEjb.createProduct(product);
productList = productEjb.findAllProducts();
return "listProduct?faces-redirect=true";
}
public String doDeleteProduct()
{
productEjb.deleteProduct(product);
return "deleteProduct?faces-redirect=true";
}
public String cancelDeleteAction()
{
return "listProduct?faces-redirect=true";
}
@PostConstruct
public void init()
{
categoryList = categoryEjb.findAllCategory();
productList = productEjb.findAllProducts();
}
Category Entity (Getters, setters, hash() and constructors removed for simplicity):
@Entity
@NamedQueries({
@NamedQuery(name= "findAllCategory", query="SELECT c FROM Category c")
})
public class Category implements Serializable
{
private static final long serialVersionUID = 1L;
@Id @GeneratedValue(strategy= GenerationType.AUTO)
private int category_id;
private String name;
private String description;
@OneToMany(mappedBy = "category_fk")
private List<Product> product_fk;
// readObject() and writeObject()
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException
{
// default deserializer
ois.defaultReadObject();
// read the attributes
category_id = ois.readInt();
name = (String)ois.readObject();
description = (String)ois.readObject();
}
private void writeObject(ObjectOutputStream oos) throws IOException, ClassNotFoundException
{
// default serializer
oos.defaultWriteObject();
// write the attributes
oos.writeInt(category_id);
oos.writeObject(name);
oos.writeObject(description);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Category other = (Category) obj;
if (this.category_id != other.category_id) {
return false;
}
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
return false;
}
if ((this.description == null) ? (other.description != null) : !this.description.equals(other.description)) {
return false;
}
if (this.product_fk != other.product_fk && (this.product_fk == null || !this.product_fk.equals(other.product_fk))) {
return false;
}
return true;
}
Product Entity (Getters, setters, hash() and constructors removed for simplicity):
@Entity
@NamedQueries({
@NamedQuery(name="findAllProducts", query = "SELECT p from Product p")
})
public class Product implements Serializable
{
private static final long serialVersionUID = 1L;
@Id @GeneratedValue(strategy= GenerationType.AUTO)
private int product_id;
private String name;
private String description;
protected byte[] imageFile;
private Float price;
@Temporal(TemporalType.TIMESTAMP)
private Date dateAdded;
@ManyToOne
private Category category_fk;
@ManyToOne
private SaleDetails saleDetails_fk;
// readObject() and writeObject() methods
private void readObject (ObjectInputStream ois)throws IOException, ClassNotFoundException
{
// default deserialization
ois.defaultReadObject();
// read the attributes
product_id = ois.readInt();
name = (String)ois.readObject();
description = (String)ois.readObject();
for(int i=0; i<imageFile.length; i++ )
{
imageFile[i]=ois.readByte();
}
price = ois.readFloat();
dateAdded = (Date)ois.readObject();
category_fk = (Category)ois.readObject();
saleDetails_fk = (SaleDetails)ois.readObject();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Product other = (Product) obj;
if (this.product_id != other.product_id) {
return false;
}
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
return false;
}
if ((this.description == null) ? (other.description != null) : !this.description.equals(other.description)) {
return false;
}
if (!Arrays.equals(this.imageFile, other.imageFile)) {
return false;
}
if (this.price != other.price && (this.price == null || !this.price.equals(other.price))) {
return false;
}
if (this.dateAdded != other.dateAdded && (this.dateAdded == null || !this.dateAdded.equals(other.dateAdded))) {
return false;
}
if (this.category_fk != other.category_fk && (this.category_fk == null || !this.category_fk.equals(other.category_fk))) {
return false;
}
if (this.saleDetails_fk != other.saleDetails_fk && (this.saleDetails_fk == null || !this.saleDetails_fk.equals(other.saleDetails_fk))) {
return false;
}
return true;
}
private void writeObject(ObjectOutputStream oos) throws IOException, ClassNotFoundException
{
// default serialization
oos.defaultWriteObject();
// write object attributes
oos.writeInt(product_id);
oos.writeObject(name);
oos.writeObject(description);
oos.write(imageFile);
oos.writeFloat(price);
oos.writeObject(dateAdded);
oos.writeObject(category_fk);
oos.writeObject(saleDetails_fk);
}
This is the stacktrace:
javax.faces.el.EvaluationException: java.lang.StackOverflowError
at javax.faces.component.MethodBindingMethodExpressionAdapter.invoke(MethodBindingMethodExpressionAdapter.java:102)
at com.sun.faces.application.ActionListenerImpl.processAction(ActionListenerImpl.java:102)
at javax.faces.component.UICommand.broadcast(UICommand.java:315)
at javax.faces.component.UIViewRoot.broadcastEvents(UIViewRoot.java:794)
at javax.faces.component.UIViewRoot.processApplication(UIViewRoot.java:1259)
at com.sun.faces.lifecycle.InvokeApplicationPhase.execute(InvokeApplicationPhase.java:81)
at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:118)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:593)
at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1550)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:281)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:175)
at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:655)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:595)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:161)
at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:331)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:231)
at com.sun.enterprise.v3.services.impl.ContainerMapper$AdapterCallable.call(ContainerMapper.java:317)
at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:195)
at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:860)
at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:757)
at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:1056)
at com.sun.grizzly.http.DefaultProtocolFilter.execute(DefaultProtocolFilter.java:229)
at com.sun.grizzly.DefaultProtocolChain.executeProtocolFilter(DefaultProtocolChain.java:137)
at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:104)
at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:90)
at com.sun.grizzly.http.HttpProtocolChain.execute(HttpProtocolChain.java:79)
at com.sun.grizzly.ProtocolChainContextTask.doCall(ProtocolChainContextTask.java:54)
at com.sun.grizzly.SelectionKeyContextTask.call(SelectionKeyContextTask.java:59)
at com.sun.grizzly.ContextTask.run(ContextTask.java:71)
at com.sun.grizzly.util.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:532)
at com.sun.grizzly.util.AbstractThreadPool$Worker.run(AbstractThreadPool.java:513)
at java.lang.Thread.run(Thread.java:722)
Caused by: java.lang.StackOverflowError
at java.util.Vector$Itr.<init>(Vector.java:1120)
at java.util.Vector.iterator(Vector.java:1114)
at java.util.AbstractList.hashCode(AbstractList.java:540)
at java.util.Vector.hashCode(Vector.java:988)
at org.eclipse.persistence.indirection.IndirectList.hashCode(IndirectList.java:460)
at com.lv.Entity.Category.hashCode(Category.java:96)
at com.lv.Entity.Product.hashCode(Product.java:148)
at java.util.AbstractList.hashCode(AbstractList.java:541)
StackOverflowError is an error which Java doesn't allow to catch, for instance, stack running out of space, as it's one of the most common runtime errors one can encounter.
Your Category
class has list of Products
and in the equals
method of the Category
class you are doing
if (this.product_fk != other.product_fk && (this.product_fk == null || !this.product_fk.equals(other.product_fk))) {
return false;
}
which invokes the equals
method on the Product
class, the equals
method on the Product
class which does
if (this.category_fk != other.category_fk && (this.category_fk == null || !this.category_fk.equals(other.category_fk))) {
return false;
}
which invokes the equals
method on the Category
once again and the whole process repeats causing an Stack overflow.
Solution:
Hope it helps.
I am suspecting circular dependency as the root cause of the issue. I am thinking you have mapped Product
either in Category
or SaleDetails
or both the objects. If so, it will call circular reference issue while serializing the Product
object, while will result into StackOverFlow
error.
I think you have two options:
bi-dreictional
mapping if it can be avoided.readObject()
and writeObject()
methods in your Product
, Category
and SaleDetails
classes and avoid reading/writing objects in circles.EDIT:
private void writeObject(ObjectOutputStream oos) throws IOException {
// default serialization
oos.defaultWriteObject();
// write the object attributes
oos.writeInt(product_id);
oos.writeObject(name);
oos.writeObject(description);
oos.write(imageFile);
oos.writeFloat(price);
oos.writeObject(dateAdded);
oos.writeObject(category_fk);
oos.writeObject(saleDetails_fk);
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
// default deserialization
ois.defaultReadObject();
//read the attributes
product_id = ois.readInt();
name = (String)ois.readObject();
description = (String)ois.readObject();
imageFile = ois.read();
price = ois.readFloat();
dateAdded = (Date)ois.readObject();
category_fk = (Category)ois.readObject();
saleDetails_fk = (SaleDetails)ois.readObject();
}
Hope this helps.
As @Sajan mentioned. You have a cyclic dependency in your equals(). You need to change the equals() method in your Category class not to refer to to 'Product' list and 'categoryId' . It should probably be as follows -
public class Category implements Serializable
{
...
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
return false;
}
if ((this.description == null) ? (other.description != null) : !this.description.equals(other.description)) {
return false;
}
return true;
}
}
In the equals() method in Product class you may have to remove 'ProductId' and 'Price'.
The equals() and hashcode() methods are important because they determine object equality when you use objects in detached state and add them to a java.util.Set. The recommended implementation is to use 'properties that form a natural key identifier' in your equals() and hashcode() implementations.
Category - Let's say you had CategoryA with ProductA and ProductB. It is not possible that the same ProductA and ProductB will be tied to a different category called CategoryB. So, they should not be part of the equals() implementation.
Product - Whether to include 'Category' in Product.equals() depends on whether 'Category' is part of the natural key identifier for a Product. And the fact that you are using a List inside Category means that you are not too concerned about object equality. If you are concerned about equality(), I would recommend that you change it to a Set.
If you had the following scenario -
Category -
Electronics
Product1 -
name - Camera | price - $100 | category - Electronics
Product2 -
name - HandyCam | Price - $200 | Category - Electronics
If 'Electronics' category has Set of two products as shown above. If you had the following sample code -
session.startTransaction();
Category electronics = session.get(Category.class, 1234);
Set<Product> products = electronics.getProducts();
session.commit();
Product camera = product.get(0);
camera.setPrice("300");
products.add(camera);
When you change the price of camera and add it back in to the Set, you want to make sure that the set still contains only two elements and not add a third new element because you are modifying an existing product, not adding a new Product.
For the above scenario, you need to have 'Category' and 'name' in the equals() method of 'Product'.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With