Java堂  


Spring MVC中修改校验的异常信息

Filed under: JavaPlateform — Jet @ 4:58 下午
Tags:
原文出处: Spring MVC中修改校验的异常信息
作者: Jet Mah from Java堂
声明: 可以非商业性任意转载, 转载时请务必以超链接形式标明文章原始出处、作者信息及此声明!

使用一个例子来说明一下,Spring使用的是3.0.x:

  1. public class Account {
  2.     @Pattern(regexp = "[a-z0-9]{3,50}")
  3.     private String name;
  4.  
  5.     @NotNull
  6.     @NumberFormat(style=Style.CURRENCY)
  7.     private BigDecimal balance = new BigDecimal("1000");
  8.  
  9.     @DateTimeFormat(style="S-")
  10.     @Future
  11.     private Date renewalDate = new Date(new Date().getTime() + 31536000000L);
  12.  
  13.     // 省略 getter & setter...
  14. }

接下来使用注解创建一个Controller:

  1. @Controller
  2. @RequestMapping(value="/account")
  3. public class AccountController {
  4.  
  5.     /**
  6.      * 如果view采用JSP的话<code>@ModelAttribute</code>注解可以省略,但是如果是使用Velocity或FreeMarker
  7.      * 的话必须要加上,而且<code>@Valid</code>注解和后面的BindingResult类型的参数之间不能有其他参数。
  8.      */
  9.     @RequestMapping(method=RequestMethod.POST)
  10.     public String create(@Valid @ModelAttribute("account") Account account, BindingResult result) {
  11.         if (result.hasErrors()) {
  12.             return "account/createForm";
  13.         }
  14.         // ...
  15.         return "redirect:/account/" + account.getId();
  16.     }
  17.  
  18. }

另外还有一个view文件、配置文件等等,具体的可以查看Spring MVC Basic Sample

OK,接下来说重点。我参考上面的Spring MVC Basic Sample中的做法也做了一个类似的表单,在表单中的balance中输入非数字的时候旁边的错误信息会出现下面的异常信息:

Failed to convert property value of type java.lang.String to required type java.math.BigDecimal for property balance; nested exception is org.springframework.core.convert.ConversionFailedException: Unable to convert value “eff” from type java.lang.String to type java.math.BigDecimal; nested exception is java.lang.IllegalArgumentException: Unable to parse eff

这个就很郁闷了,明显是格式转换出错,但是列出来一堆的异常信息,对用户来说非常不友好。于是开始在Google中搜索也没有找到比较好的说法,后来运行了Spring MVC Basic Sample,不显示上面的异常信息,而只是显示一句话:”could not be parsed“,这个效果正是我们所期待的。于是我将例子中的所有spring配置文件、Bean信息、Controller都做了对比,甚至JSP模板都一样,但是一到我自己的项目中就会出现那个异常,当时非常郁闷。

后来无意中在Google中查到到了参考资料中的一篇帖子,里面提到只要在messages.properties文件中设置typeMismatch属性就可以了,于是我猛然想到Spring MVC Basic Sample中也有个messages.properties文件,打开之后赫然看到果然有一条typeMismatch属性:

  1. typeMismatch=could not be parsed

原来是在这里设置的,真的很郁闷。还好,参考资料中的帖子中还提到可以设置具体的类型,比如可以这样设置:

  1. ## 全局的配置信息
  2. typeMismatch=输入的数据格式不正确
  3. ## 针对BigDecimal类型的错误信息
  4. typeMismatch.java.math.BigDecimal=请输入数字
  5. ## 针对Date类型的错误信息
  6. typeMismatch.java.util.Date=请在{0}中输入正确的日期

参考资料:
Spring MVC Data Binding and user-friendly error messages

给Spring Security中的j_spring_security_check等改名

Filed under: JavaPlateform — Jet @ 11:43 上午
原文出处: 给Spring Security中的j_spring_security_check等改名
作者: Jet Mah from Java堂
声明: 可以非商业性任意转载, 转载时请务必以超链接形式标明文章原始出处、作者信息及此声明!

默认情况下,Spring Security(SS)的登录验证提交的Servlet默认名称为j_spring_security_check,但是很多情况下想将这个名称修改掉,后来终于在Spring的官方论坛上找到了一个哥们儿提出的类似的问题(见参考资料),原来只要修改<form-login>节点login-processing-url属性就可以了,其他的一些设置可以参考官方的文档:B.1.5 The <form-login> Element

对于SS的使用及配置文件的含义可以参考这篇文章:spring-security3 配置和使用,总结的非常到位了。

参考资料:
Rename j_spring_security_check to j_security_check

SimpleJdbcInsert使用executeAndReturnKeyHolder方法返回主键时需要注意的一个地方

Filed under: JavaPlateform — Jet @ 1:32 下午
Tags:
原文出处: SimpleJdbcInsert使用executeAndReturnKeyHolder方法返回主键时需要注意的一个地方
作者: Jet Mah from Java堂
声明: 可以非商业性任意转载, 转载时请务必以超链接形式标明文章原始出处、作者信息及此声明!

Spring的SimpleJdbcInsert发挥了Simple风格,与SimpleJdbcTemplate同属于Simple体系。该类为向数据库中插入数据提供了一个非常快捷的方式,另外它还提供了一套用于返回插入数据的主键的方法:executeAndReturnKeyHolder、executeAndReturnKey。

查看API的时候可以看到executeAndReturnKey这个方法的返回类型是Number类型,当时我就再想如果主键的类型是String类型呢,比如UUID。后来看到还有一个executeAndReturnKeyHolder方法,返回的是一个KeyHolder对象,可以通过keyHolder#getKeys()获取主键的值,另外还有一个getKeyList()方法用于复合主键的情况,这里先撇开不说。

看完API之后那就可以动手了,代码如下:

  1. // jdbcInsert是SimpleJdbcInsert对象
  2. Map<String, Object> data = Maps.newHashMap();
  3. data.put("id", "t0001");
  4. data.put("name", "Tom");
  5. data.put("age", 24);
  6. KeyHolder keyHolder = jdbcInsert.withTableName("t_tablename")
  7.         .usingColumns("id", "name", "age")
  8.         .usingGeneratedKeyColumns("id")
  9.         .executeAndReturnKeyHolder(data);
  10. // 下面主要是对keyHoder进行分析
  11. if(keyHolder == null) {
  12.     return null;
  13. }
  14. Map<String, Object> keys = keyHolder.getKeys();
  15. if(keys == null || keys.size() == 0 || keys.values().size() == 0) {
  16.     return null;
  17. }
  18. Object key = keys.values().toArray()[0];
  19. if(key == null || !(key instanceof Serializable)) {
  20.     return null;
  21. }
  22. if(key instanceof Number) {
  23.     Long k = (Long)key;
  24.     return (idType == int.class || idType == Integer.class) ?
  25.             k.intValue() : k;
  26. } else if(key instanceof String) {
  27.     return (String)key;
  28. } else {
  29.     return (Serializable)key;
  30. } // end of if(key instanceof Number)

如果主键id的类型是int或long上面的代码没有任何问题,但是如果是自定义的UUID等String类型则问题出现了,提示下面的错误:

org.springframework.jdbc.UncategorizedSQLException: PreparedStatementCallback; uncategorized SQLException for SQL []; SQL state [HY000]; error code [1364]; Field ‘id’ doesn’t have a default value; nested exception is java.sql.SQLException: Field ‘id’ doesn’t have a default value

有些奇怪了吧?明明id的值已经传入了,但是错误的提示应该是没有传入id的值。进入到Spring的源代码中,发现代码里面有一些debug信息,于是在log4j中将debug打开:

  1. log4j.logger.org.springframework.jdbc.core=debug

从打印出来的信息中看,Spring自动生成的Insert语句中竟然没有id字段!!!继续最终源代码,先在org.springframework.jdbc.core.simple.AbstractJdbcInsert类中找到protected void compileInternal()方法,在代码前加上一个debug信息:

  1. protected void compileInternal() {
  2.     logger.debug("getGeneratedKeyNames: " + getColumnNames());
  3.     tableMetaDataContext.processMetaData(getJdbcTemplate().getDataSource(), getColumnNames(), getGeneratedKeyNames());
  4.     ...
  5. }

这个时候打印的列表中有id字段,继续最终,最后终于在TableMetaDataContext#createInsertString(java.lang.String[])方法里面找到,关键的代码片段如下:

  1. for (String columnName : this.getTableColumns()) {
  2.             // 这里将SimpleJdbcInsert#usingGeneratedKeyColumns方法中所设置的字段去除了
  3.             if (!keys.contains(columnName.toUpperCase())) {
  4.                 columnCount++;
  5.                 if (columnCount > 1) {
  6.                     insertStatement.append(", ");
  7.                 }
  8.                 insertStatement.append(columnName);
  9.             }
  10.         }

看到这里,我也猛然恍然大悟了。既然在INSERT INTO语句中设置了UUID的值,那这里就不需要再使用KeyHolder进行返回了,直接获取就是了。这也是为什么KeyHolder中的getKey()方法的返回类型是Number的原因了,因为通常来说需要Spring返回的就是插入数据库中的自增类型的主键值。

Spring+Velocity中模板路径的问题

Filed under: JavaPlateform — Jet @ 1:54 下午
原文出处: Spring+Velocity中模板路径的问题
作者: Jet Mah from Java堂
声明: 可以非商业性任意转载, 转载时请务必以超链接形式标明文章原始出处、作者信息及此声明!

在Spring中使用Velocity进行视图渲染的时候需要注意一个路径的设置问题。Spring本身提供了一个用于对Velocity进行设置的类,我们做如下设置:

  1. <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  2.         <property name="configLocation" value="/WEB-INF/velocity.properties" />
  3.         <property name="resourceLoaderPath" value="/WEB-INF/templates" />
  4.     </bean>

使用configLocation属性设置了velocity配置文件的路径及文件名,而resourceLoaderPath设置了模板文件所在的位置。这里我们看到,所有的设置路径都是基于网站根目录的。

这个时候对于velocity.properties中用于设置宏文件的velocimacro.library选项而言,他所对应的目录就是前面resourceLoaderPath中设置的目录了,比如我们可以设置如下:

  1. velocimacro.library = macro.vm

并且对于模板文件中#parse指令所包含的文件也是基于resourceLoaderPath中所设置的目录。

如果我们将resourceLoaderPath设置为网站跟目录,而模板文件放在了/WEB-INF/template下,这个时候velocimacro.library和#parse指令所包含的文件路径就是基于网站根目录了,例如有个section.vm文件放在了/WEB-INF/template目录下,这个时候在模板文件中必须写成#parse(“/WEB-INF/template/section.vm”)才可以,可以看出这样非常的麻烦。

因此我们不能想当然的将resourceLoaderPath设置为网站的根目录,而是直接指向模板文件所在的目录。或许你认为我这么说有些可笑,但是有的时候我们往往在这些看似不经意的问题上浪费时间。