本文描述了如何统一的增加用户输入长度校验以及自己的思考过程
1、需求
在理想情况下,无论是前端还是后台都应该对用户输入的长度做检验。在每个页面和后台的接口都加上检验,比较繁琐,而且编码人员经常忽视检验。造成用户输入的长度大于数据库设定的长度就会直接报错。因此希望在框架上做统一校验。
2、分析
系统采用的是spring+spring mvc+mybatis+mysql。
分析前,先整理下新增或修改请求的大致流程:
1.客户端提交数据,如:/rest/user/add?name=张三&age=18。如果字段比较多通常用json格式post,格式:{name:"张三",age:18}
2.无论何种形式提交过来,后台先由spring的拦截器拦截,主要做登录验证,确保请求身份的合法性。
3.如果是合法请求,会根据请求地址跳到对应的controller里面。
4.controller调用对应的业务类。在该类里,会根据name,age等参数,创建一个user类型的po对象。
在我们的项目里,数据库里的每一张表都会有一个po实体对象与之对应,这些对象都由mybatis generator 统一生成。与po对象同时生成的还有mapper和sql配置文件。
5.调用mapper的insert 方法,代码类似于:
User u = new User();
u.setName(user);
u.setAge(age);
u.setUserId(GUID);
userMapper.insert(user);//保存到数据库
6.在执行sql之前会先经过mybatis的插件/拦截器
7.最后调用mybatis里的 Executor对象的update方法,执行sql语句。
大致这7步,一些细致末节已略去。接下来便是分析思路(嫌烦的朋友可以直接看第三部分的实现):
首先既然是统一的校验,肯定是放在公共的地方,每个接口都会调用。最合适的地方便是第2或6步骤,通过拦截器统一处理。2和6里面,2其实是最合适的,因为如果检验不通过,直接返回给客户端,效率最高。
检验前需要获取用户输入的长度和数据库里设定的长度。获取这两个长度应该都没问题。但步骤2却有个问题,提交过来的参数各式各样,将参数和数据库字段对应起来有难度。因此先放弃该方案。
再来分析步骤6。在项目里所有的更新或新增方法数据都是用po对象传递过去(个别直接写sql语句的除外),如:userMapper.insert(user);这里的参数user就是一个po对象。而po对象有个好处就是它里面的字段都可以和数据库的字段对应起来。
找到这个突破口似乎看到了一丝曙光。在执行sql之前的拦截方法里面我们应该可以拿到po对象。进而我们就得到了用户的输入长度。如果用户输入有多项,我们只要通过反射遍历po对象的每个属性就可以了。
到这里我们还缺数据库字段的长度。因为上面提到需要反射遍历po对象的所有属性,所以第一个想法就是把长度用注解的方式加在字段上,这样就可以直接比较了,如:
public class User implements Serializable {
/**
* 用户登录名
*
* @mbggenerated
*/
private String username;
/**
* 登录手机号
*
* @mbggenerated
*/
private String phone;
/**
* 获取 用户登录名
*
* @return 用户登录名
*
* @mbggenerated
*/
@org.beanopen.fw.ws.annotation.CodeFieldNotes("用户登录名")
@org.beanopen.tools.mybatis.annotation.DBColumnLength(45)
public String getUsername() {
return username;
}
/**
* 获取 登录手机号
*
* @return 登录手机号
*
* @mbggenerated
*/
@org.beanopen.fw.ws.annotation.CodeFieldNotes("登录手机号")
@org.beanopen.tools.mybatis.annotation.DBColumnLength(11)
public String getPhone() {
return phone;
}
}
这里每个字段都有两个注解,字段名称和长度。
到这一步感觉胜利在望了。但每个字段的注解都手工配置,那也不可行。好在上文已经提到,po对象都是mybatis generator里生成,mybatis generator也提供了插件机制,允许生成时,添加用户自己的逻辑。那么生成实体对象时添加字段程度的注解即可。
以上思路总结为:
1.mybatis generator 生成实体对象时,为每个字符串属性添加关于字段长度的自定义注解。
2.mybatis 执行sql方法之前进行拦截,遍历po对象的每个字符串属性,比较用户输入长度和注解长度。
3.如果过长,则抛出异常。后台进行统一的异常处理返回友好的错误提示。
3.代码实现
首先是mybatis generator的插件部分:
package org.beanopen.tools.mybatis.generator;
import java.util.List;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.Plugin;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.TopLevelClass;
public class DBColumnLengthPlugin extends PluginAdapter {
public DBColumnLengthPlugin() {
}
public boolean modelGetterMethodGenerated(Method method, TopLevelClass topLevelClass,
final IntrospectedColumn introspectedColumn, IntrospectedTable introspectedTable,
Plugin.ModelClassType modelClassType) {
List<IntrospectedColumn> pkcs = introspectedTable.getPrimaryKeyColumns();
if("VARCHAR".equals(introspectedColumn.getJdbcTypeName())
&&pkcs.indexOf(introspectedColumn)<0){
//数据库字段为varchar类型的,且不是主键的属性上添加注解
method.addAnnotation("@org.beanopen.tools.mybatis.annotation.DBColumnLength("
+introspectedColumn.getLength()
+ ")");
}
return true;
}
/**
* This plugin is always valid - no properties are required
*/
@Override
public boolean validate(List<String> warnings) {
return true;
}
}
自定义的插件类继承于org.mybatis.generator.api.PluginAdapter,主要在modelGetterMethodGenerated方法里,getter方法上添加字段长度的注解
插件写好之后,还需要配置进去。在mybatis generator的配置文件generatorConfig.xml里添加插件<plugin type="org.beanopen.tools.mybatis.generator.DBColumnLengthPlugin" />:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration>
<classPathEntry location="conf/lib/mysql-connector-java-5.1.15.jar" />
<context id="context1">
<property name="javaFileEncoding" value="UTF-8"/>
<plugin type="org.beanopen.tools.mybatis.generator.MySQLPaginationPlugin2" />
<!-- 添加刚才写好的插件 -->
<plugin type="org.beanopen.tools.mybatis.generator.DBColumnLengthPlugin" />
<plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
………………
</context>
</generatorConfiguration>
到这里生成实体的时候就会自动添加注解了。接下来便是mybatis的拦截代码:
package org.beanopen.tools.mybatis;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.beanopen.fw.ws.annotation.CodeFieldNotes;
import org.beanopen.tools.mybatis.annotation.DBColumnLength;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;
@Intercepts(@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class}))
public class DBColumnLengthCheck implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object arg = invocation.getArgs()[1];
Field[] fields = arg.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {//遍历每个属性
Field field = fields[i];
if(field.getType() == String.class) {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), arg.getClass());
Method getMethod = pd.getReadMethod();
if (getMethod != null) {
DBColumnLength length = getMethod.getAnnotation(DBColumnLength.class);
if(length!=null) {//如果有DBColumnLength注解,则需要判断
Object value = getMethod.invoke(arg);
if(value!=null){
if(length.value()<value.toString().length()){//输入内容过长时抛出异常
CodeFieldNotes fieldNotes = getMethod.getAnnotation(CodeFieldNotes.class);//CodeFieldNotes 注解是字段名称,为了显示友好而已,这里可忽略
String fieldName = fieldNotes!=null&& StringUtils.isNotBlank(fieldNotes.value())?fieldNotes.value():field.getName();
throw new RuntimeException(String.format("\"%s\"输入项不能多于%s个字符,您已输入%s个",fieldName,length.value(),value.toString().length()));
}
}
}
}
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
这里类注解@Intercepts @Signature指定了要拦截Executor类的update方法。mybatis貌似有4个类可以拦截,Executor类是其中一个
我们重写了intercept方法,添加了一段检验代码,如果检验通过则调用invocation.proceed(),继续原有逻辑;反之抛出异常。
将插件配置进去<bean class="org.beanopen.tools.mybatis.DBColumnLengthCheck"> </bean>:
<!-- 数据库会话工厂 -->
<bean name="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:config/mybatis-configuration.xml"></property>
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath*:cn/bodtec/zzb/mappers/**/*.xml" />
<property name="plugins">
<list>
<bean
class="org.beanopen.tools.mybatis.pagination.PaginationInterceptor">
<property name="dialect">
<bean class="org.beanopen.tools.mybatis.pagination.dialect.MySQLDialect" />
</property>
</bean>
<!-- 添加插件 -->
<bean class="org.beanopen.tools.mybatis.DBColumnLengthCheck"> </bean>
</list>
</property>
</bean>
主要代码算是完成了,为了让程序更友好,可以再对异常做全局处理。代码就不放了。
4.总结
此方案比较简单,日常开发中,只需建表的时候将字段名称和长度定好就可以了,其他什么都不用做。后台将会自动进行校验。缺点是性能稍差,在执行sql前才做校验。期待能有更好的方案。