4

I keep getting an error 500 from Tomcat when I return a list of entities from my service layer in Spring. If I just make the entities on the fly in memory I don't get the error and they return fine, so it's something to do specifically with the transactions.

I have the following method in my controller...

@RequestMapping(value = "{examid}", method = RequestMethod.GET)
@ResponseBody
public List<Exam> getExam(@PathVariable String examid, Model model) {
    return examService.getAllExams();
}

When this method gets invoked, tomcat displays a error 500 and no stack trace at all. I've even wrapped it in a try catch and nothing appears in that stacktrace.

This service just calls a dao...

@Service("examService")
public class ExamServiceImpl implements ExamService{

    @Autowired
    private ExamDao examDao;

        @Override
        @Transactional(readOnly = true)
        public List<Exam> getAllExams() {
            return examDao.getAllExams();
        }

And this dao just returns a simple HQL script...

@Repository("examDao")
public class ExamDaoImpl implements ExamDao {
@Autowired
    private SessionFactory sessionFactory;

    @Override
    public List<Exam> getAllExams() {
        return sessionFactory.getCurrentSession().createQuery("from Exam")
                .list();
    }

Originally, I thought it was something to do with the entities being lazy loaded, my entities are below as you can see, they're very simple

@Entity
@Table(name = "exams")
public class Exam implements Serializable {

    private static final long serialVersionUID = -5109075534794721930L;

    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid", strategy = "uuid")
    @Column(name = "examid", unique = true)
    private String examId;

    @OneToMany(mappedBy = "exam")
    private Set<Question> questions;

    public String getExamId() {
        return examId;
    }

    public void setExamId(String examId) {
        this.examId = examId;
    }

    /**
     * @return the questions
     */
    @JsonManagedReference
    public Set<Question> getQuestions() {
        return questions;
    }

    /**
     * @param questions the questions to set
     */
    public void setQuestions(Set<Question> questions) {
        this.questions = questions;
    }

}

Question entity

@Entity
@Table(name = "questions")
public class Question  implements Serializable  {

    private static final long serialVersionUID = 8804841647267857850L;
    private String questionId;
    private Exam exam;
    private Set<Answer> answers;

    /**
     * @return the answer
     */
    @JsonManagedReference
    @OneToMany(mappedBy = "question")
    public Set<Answer> getAnswers() {
        return answers;
    }

    /**
     * @param answer
     *            the answer to set
     */
    public void setAnswers(Set<Answer> answers) {
        this.answers = answers;
    }

    /**
     * @return the exam
     */
    @JsonBackReference
    @ManyToOne
    @JoinColumn(name = "examid", nullable = false)
    public Exam getExam() {
        return exam;
    }

    /**
     * @param exam
     *            the exam to set
     */
    public void setExam(Exam exam) {
        this.exam = exam;
    }

    /**
     * @return the questionId
     */
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    @Column(name = "questionid", unique = true)
    public String getQuestionId() {
        return questionId;
    }

    /**
     * @param questionId
     *            the questionId to set
     */
    public void setQuestionId(final String questionId) {
        this.questionId = questionId;
    }
}

Answer entity

@Entity
@Table(name = "answers")
public class Answer  implements Serializable {

    private static final long serialVersionUID = -3922870865886851018L;

    @Id
    @GeneratedValue(generator="system-uuid")
    @GenericGenerator(name="system-uuid", strategy = "uuid")
    @Column(name = "answerid", unique = true)
    private String answerId;

    @ManyToOne()
    @JoinColumn(name = "questionid", nullable = false)
    private Question question;

    /**
     * @return the question
     */
    @JsonBackReference
    public Question getQuestion() {
        return question;
    }

    /**
     * @param question the question to set
     */
    public void setQuestion(Question question) {
        this.question = question;
    }

    /**
     * @return the answerId
     */
    public String getAnswerId() {
        return answerId;
    }

    /**
     * @param answerId the answerId to set
     */
    public void setAnswerId(final String answerId) {
        this.answerId = answerId;
    }

}

I'm using Jackson 2.2.0 - now the following code works fine, so it's definitely some issue with either the joins (which were originally lazy loaded but removed for debugging). Just returning the following code from the controller works fine...

        Exam exam = new Exam();
        Question presidentQuestion = new Question();
        presidentQuestion.setExam(exam);
        Question skyQuestion = new Question();
        skyQuestion.setExam(exam);
        Set<Question> questions = new HashSet<>();
        questions.add(skyQuestion);
        questions.add(presidentQuestion);
        exam.setQuestions(questions);
        Answer answer = new Answer();
        answer.setQuestion(skyQuestion);
        Answer answer1 = new Answer();
        answer1.setQuestion(skyQuestion);
        Answer answer2 = new Answer();
        answer2.setQuestion(presidentQuestion);
        Set<Answer> answers1 = new HashSet<>();
        answers1.add(answer2);
        Set<Answer> answers = new HashSet<>();
        answers.add(answer);
        answers.add(answer1);
        presidentQuestion.setAnswers(answers1);
        skyQuestion.setAnswers(answers);
            return Arrays.asList(exam);

There's nothing in the Tomcat logs - how can I force some sort of stacktrace? Hibernate version = 4.1.10.Final, Spring version 3.1.4.

david99world
  • 18,271
  • 28
  • 102
  • 127
  • 1
    Have you got log4j or equivalent configured? You should be able to get a lot of logging out of Spring and Hibernate to help diagnose this problem. Failing that, is it possible you can make the whole project available somewhere so I can attempt to reproduce locally? – andyb Jun 03 '13 at 21:06
  • 1
    Yeah I've got the logging turned on - even debugging it just returns every as expected, no stacktrace etc. I'll put the project in the question. – david99world Jun 04 '13 at 16:39

1 Answers1

6

Got it working with a few changes…

1) The main/resources/log4j.xml should be set to debug to see the error:

<logger name="org.springframework.context">
  <level value="debug" />
</logger>

<logger name="org.springframework.web">
  <level value="debug" />
</logger>

<!-- Root Logger -->
<root>
  <priority value="debug" />
  <appender-ref ref="console" />
</root>

I was mistakenly editing the test/resources/log4j.xml first. Perhaps you did the same? Otherwise maybe your IDE is getting in the way, because as soon as I set the correct level on the log4j.xml I was seeing the logging in the terminal just fine.

Initially I thought the error was because Chrome was asking for a /favicon.ico immediately after the login and Spring Security was denying access.

DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/favicon.ico'; against '/'

DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/favicon.ico'; against '/exam**'

DEBUG: org.springframework.security.web.util.AntPathRequestMatcher - Checking match of request : '/favicon.ico'; against '/exam/**'

DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /favicon.ico; Attributes: [denyAll]

DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fb77dfdd: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 1qtptnbw65j3g145c9ni63ucw1; Granted Authorities: ROLE_ADMIN, ROLE_USERDEBUG: org.springframework.security.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@96f613, returned: -1

DEBUG: org.springframework.security.web.access.ExceptionTranslationFilter - Access is denied (user is not anonymous); delegating to AccessDeniedHandlerorg.springframework.security.access.AccessDeniedException: Access is denied

I added <http pattern="/favicon.ico" security="none" /> to the security-context.xml to remove this problem, although it turned out to be something else after adding that.

The next error was the dreaded "failed to lazily initialize a collection of" which has been covered many times on SO, for example:

DEBUG: org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - Resolving exception from handler [public java.util.List com.securetest.app.controller.ExamController.getExam(java.lang.String,org.springframework.ui.Model)]: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: failed to lazily initialize a collection of role: com.securetest.app.entity.Exam.questions, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]->com.securetest.app.entity.Exam["questions"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: com.securetest.app.entity.Exam.questions, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]->com.securetest.app.entity.Exam["questions"])

DEBUG: org.springframework.web.servlet.DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'appServlet': assuming HandlerAdapter completed request handling

DEBUG: org.springframework.web.servlet.DispatcherServlet - Successfully completed request

I solved this the quick way of just manually getting the related data whilst still in the transaction with:

@Override
@Transactional(readOnly = true)
public List<Exam> getAllExams() {
  List<Exam> exams = examDao.getAllExams();
  for (Exam e : exams) {
    for (Question q : e.getQuestions()) {
      q.getAnswers().size();
    }
  }
  return exams;
}

which allowed the request to complete successfully.

Hope it helps!

Community
  • 1
  • 1
andyb
  • 42,062
  • 11
  • 113
  • 146