使用泛型Form Bean类的Spring ClassCastException

时间:2011-11-16 15:25:45

标签: java spring generics

我正在尝试使用泛型类作为Spring表单支持bean,但是当Spring框架尝试将Object强制转换为实际类型时,最终会出现ClassCastException。

提交表单时,尝试在SrvRecord对象上调用方法时会出现以下错误(第105行,标记为注释):

java.lang.ClassCastException: java.lang.Object cannot be cast to com.[...].portal.entity.SrvRecord
        at com.[...].portal.controller.SrvController.add(SrvController.java:105)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.invokeHandlerMethod(HandlerMethodInvoker.java:176)
        at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:436)
        at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:424)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:790)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:719)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:669)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:585)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
        at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
        at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:390)
        at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
        at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
        at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
        at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:440)
        at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230)
        at org.mortbay.jetty.handler.HandlerCollection.handle(HandlerCollection.java:114)
        at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
        at org.mortbay.jetty.Server.handle(Server.java:326)
        at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
        at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943)
        at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756)
        at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
        at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
        at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:410)
        at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)

Form Bean:

public class RecordBean<T>
{

    private T original;
    private T modified;

    public RecordBean()
    {
        super();
    }

    public RecordBean(T original)
    {
        this.original = original;
        this.modified = original;
    }

    public T getOriginal()
    {
        return original;
    }

    public void setOriginal(T original)
    {
        this.original = original;
    }

    public T getModified()
    {
        return modified;
    }

    public void setModified(T modified)
    {
        this.modified = modified;
    }

}

控制器方法:

@RequestMapping(value = "new", method = RequestMethod.GET)
public String add(Model model)
{
    SrvRecord srvRecord = getSrvRecord();

    RecordBean<SrvRecord> record = new RecordBean<SrvRecord>(srvRecord);
    model.addAttribute("record", record);

    return "generic/new";
}

@RequestMapping(value = "new", method = RequestMethod.POST)
public String add(Model model, @ModelAttribute("record") RecordBean<SrvRecord> record)
{
    // Call a method on the SrvRecord object
    doSomething(record.getModified().getZone().getName());  // line 105
    doSomething(record.getOriginal().getZone().getName());

    // ...
}

查看:

<c:url value="/edit" var="formUrl" />
<form:form commandName="record" action="${formUrl}">
    <form:input type="hidden" path="original.zone" />
    <form:input type="hidden" path="original.name" />    
    <!-- ... -->

    <form:input path="modified.zone" /><br />
    <form:input path="modified.name" /><br />
    <!-- ... -->
</form:form>

任何想法或建议都会很棒。能够使用通用表单bean将从基线中消除大量不必要的代码。

仅供参考,使用的Spring版本是3.0.6.RELEASE。

谢谢, 博

4 个答案:

答案 0 :(得分:3)

你可以在Spring上提交一个bug。问题是Spring使用反射来确定参数类型,并且它们忽略了泛型,因此只是实例化没有泛型的普通RecordBean对象。因此,RecordBean中的对象只是作为Object创建,并且无法将其强制转换为SrvRecord。唯一的解决方法是不使用泛型。

<强>背景

Spring内部使用MethodParameter类来读出方法参数。有一种叫做

的方法
public Class<?> getParameterType()

调用:

this.method.getParameterTypes()[this.parameterIndex]

此代码在没有泛型的情况下读出参数。他们需要调用它来正确处理它

this.method.getGenericParameterTypes()[this.parameterIndex]

后来的方法

 HandlerMethodInvoker.resolveModelAttribute()
通过执行

调用

来实例化命令类

 bindObject = BeanUtils.instantiateClass(paramType);

但是paramType的值是“com.test.RecordBean”而不是“com.test.RecordBean&lt; SrvRecord&gt;”正如预期的那样

答案 1 :(得分:1)

您可以尝试使您的通用更具体。

public class RecordBean<T extends interfaceOrSuperclassOfSrvRecord>

答案 2 :(得分:1)

您可以通过为您的类实现检查泛型来找到错误的来源,即在对象中保留Class引用并在必要时使用显式强制转换:

public class RecordBean<T>
{
    private Class<T> clazz;
    private T original;
    private T modified;

    public RecordBean(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    public RecordBean(T original, Class<T> clazz)
    {
        this.clazz = clazz;
        this.original = original;
        this.modified = original;
    }

    public RecordBean(T original)
    {
        this(original, (Class<T>) original.getClass());
    }

    public T getOriginal()
    {
        return original;
    }

    public void setOriginal(T original) 
    {
        this.original = clazz.cast(original);
    }

    public T getModified()
    {
        return modified;
    }

    public void setModified(T modified) 
    {
        this.modified = clazz.cast(modified);
    }

}

答案 3 :(得分:0)

这里存在同样的问题,我发现的唯一解决方案是为每个控制器创建一个额外的类(FormModelObjectCity),所以我的类如下所示:

public class CityDTO {
    private Long id;
    private String name;

    public CityDTO() {

    }

    public Long getId() {
    return id;
    }

    public void setId(Long id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

}

...

public class FormModelObjectCity {
    private CityDTO data;
    private String action;

    public CityDTO getData() {
    return data;
    }

    public void setData(CityDTO data) {
    this.data = data;
    }

    public String getAction() {
    return action;
    }

    public void setAction(String action) {
    this.action = action;
    }

}

...

@Controller
public class CitiesController {

    @RequestMapping(value = "/cities/city", method = RequestMethod.GET)
    public String cityForm(Model model, FormModelObjectCity command) {
        ...
    }

    @RequestMapping(value = "/cities/city", method = RequestMethod.POST)
    public String cityFormSubmit(@ModelAttribute("command") FormModelObjectCity command, BindingResult errors, Model model) {
        ...
    }
}

form.html,在<>

中替换[]
[form action="#" th:action="@{/cities/city}" th:object="${command}" method="post"]
   [input type="hidden" th:field="*{action}"]
   [input type="hidden" th:field="*{data.id}"]
   [div class="form-group" style="width: 40%;"]
      [label th:text="#{city.name}"][/label]
      [input type="text" class="form-control" th:field="*{data.name}"]
      [small th:if="${#fields.hasErrors('data.name')}" th:errors="*{data.name}" class="form-text" style="color: red;"]&nbsp;[/small]
   [/div]
[/form]

...