那些糟糕的开发框架

编程语言 来源:tang9140 42℃ 0评论


本文章由@唐乾 出品,转载请注明出处。  
文章链接: http://blog.csdn.net/tang9140/article/details/52526977


引言


本文将与大家一起来吐槽下公司的开发框架,站在开发者的角度详述糟糕设计下导致的各种问题和使用痛点,并给出改进意见,最后附赠一个完整版后台开发框架(Spring+Spring MVC+Apache Shiro+MyBatis+Bootstrap UI)


Java目前流行的开发框架,不外乎SSH、SSM,或者两者的混搭。 集成这些框架技术本身并不难,难点在于怎么让框架简单易用,更好的服务于开发者,让开发者专注于业务而不是一些无用的设计上。下面就从Dao,Service,Controller及基础功能四个方面一一痛斥那些糟糕的设计,先从最底层dao说起

1、Dao

问题一、没有抽象实体类

由于没有抽象父类,每个实体中重复出现公共字段(如id,createBy,updateBy等)及相似方法(如toString()方法),不利于维护。解决方法就是提取出抽象父类BaseEntity,其中包含公共字段和方法,而其它实体继承该类。


在对抽象类BaseEntity进行设计时,要仔细斟酌其中包含的字段和方法,避免过于简单或复杂的设计。

一是设计过于简单,只是简单的包含公共字段。实际除了公共字段外,BaseEntity中还应该包含公共方法,如重写toString()方法如下:

    @Override
public String toString() {
return ReflectionToStringBuilder。toString(this);
}
这样每个实体就不必单独写自己的toString方法了。

 

除此之外,BaseEntit中还可以定义抽象方法,如定义以下两个抽象方法

	//插入之前执行方法,子类实现
public abstract void preInsert();
//更新之前执行方法,子类实现
public abstract void preUpdate();
通过service层的配合,在执行dao的插入或更新前,执行相应实体的preInsert()或preUpdate()方法,统一规范了实体执行数据库操作的事件方法。

 

二是设计过于复杂,包含一些不通用的字段或方法。如在BaseEntity包含parentId字段或getUrl()方法(用于获取图片访问路径)

当某几个实体需要parentId字段时,我们可以通过定义新的抽象实体XxxEntity继承自BaseEntity,然后在XxxEntity中定义parentId字段方式来实现。基本原则是通过有层次的继承关系,来保证每个层级抽象类职责单一

对于某个实体类特有的行为,则直接在该实体上新增方法即可,比如在FileInfo实体上增加getUrl()方法。

问题二、没有抽象Dao类

由于没有抽象dao类,每个dao上出现相似的CRUD(增删改查)方法声明,代码显得冗余和混乱。如插入操作,不同的开发者命名就不一样,有用insert、insertXxx、saveXxx各种名称的。这还不算乱的,命名最乱是查询操作,有getXxx、findXxx、queryXxx、loadXxx、selectXxx各种命名的,我就不一一列举了,大家都懂得。


导致上面的问题有两方面的原因:一是因为dao层设计不当;二是因为项目没有命名规范管理。下面就说怎么从dao层设计上规避上面的问题。

解决方法跟实体抽象一样,就是提取抽象父类BaseDao,其它dao继承该抽象类。 例BaseDao接口代码如下:

public interface BaseDao {

public T get(String id);

public T get(T entity);

public List findList(T entity);

public List findAllList(T entity);

public int insert(T entity);

public int update(T entity);

public int delete(T entity);

}
由于BaseDao中包含了通用的CRUD操作,继承该接口的dao也就不需要再进行方法声明,保证了dao层方法命名的统一规范。

 

补充说明:由于上面只定义了BaseDao接口,对于mybatis框架来说,已经足够使用,但是对于Hibernate框架来说,必须有对应的BaseDaoImpl方法实现,才能使dao层正常运转起来,具体代码实现可参考项目https://github.com/tylanbin/platform-ng

实现类地址: https://github.com/tylanbin/platform-ng/blob/master/src/main/java/me/lb/dao/common/impl/GenericDaoImpl.java

问题三、使用Map传递参数

dao层传递参数尽量不要使用Map集合,原因是Map隐藏了参数细节,调用者无法从方法参数上获取到有用信息,导致代码可读性差。在hibernate框架的dao查询方法上经常见到Map类型参数,应该尽量避免这种情况。可通过以下方式进行改进:

一、如果Map传递参数数量不超过两个,直接使用其中的具体参数类型进行方法声明

二、如果Map参数超过两个,建议直接使用实体类或继承实体的查询类进行传参。

2、Service

service常见问题跟dao层类似,一是没有没有抽象父类BaseService,导致大量相似的方法声明出现;二是使用Map传递参数。解决方式也跟dao层相似,具体代码参照推荐框架。下面说下service层另一个常见问题

Service之间大量相互调用

假设有两个service A和B:A调用B的cud(增删改)操作,这个是正常的,但是A要尽量少调用B的查询方法,实在需要查询时,直接调用B相应的dao进行查询。

为啥要减少service之间的相互查询调用?

一是出于查询效率的考虑。 如UserService的get()方法中除了查询用户基本信息外,还会查询相关信息,当在其它service中调用该方法时就会导致不必要的sql查询出现。在比较关注查询效率的场景下,各个service应该直接操作dao,实行按需查询。

二是出于松耦合考虑。由于service查询方法一般是为某个页面或api服务,不具有一般性,因此,应该尽量避免service代码相互耦合以提高系统扩展性。

3、Controller

与Dao,Serivice层一样,Controller层应该有抽象父类BaseController,其中包含公共字段和方法。除此之外,Controller层还需要解决以下几个问题:

表单参数如何接收?

一种不好的方式就是为每个表单定义一个EntityForm类(用来接收表单值)和FormConverter类(实现EntityForm和实体的相互转换)。这种方式会导致出现大量的类、繁琐的转换代码,并且意义也不大。这里推荐更简单直接的方法,直接用实体类或继承实体类的表单类进行接收,不需要做任何转换即可与service层无缝对接。

表单参数如何验证?

一般服务端对接收的参数都要进行验证,如有效性、安全性验证。一种不好的方式是在service中硬编码进行检验,这样对service层侵入性太强,修改也不方便。推荐通过Hibernate Validator注解,配合controller层手动调用验证方法解决。

页面传值技巧

一般直接通过实体类或ViewObject对象向页面传值。这里要注意ViewObject中应该是聚合实体类(即包含实体类型的成员变量),而不是继承实体类。对于使用mybatis框架来说,建议不要直接用sql得到Vo对象,这意味着需要在mapper.xml中定义resultMap,会导致每次复查代码时要花时间对下resultMap中的字段映射,十分讨厌。个人喜欢直接查询出实体,再通过程序聚合得到Vo,这样做既保证了 mapper.xml的简洁干净,又能满足各种页面传值展示的需要。

4、基础功能

问题一、没有统一工具类

基础工具类包括字符串处理、日期处理,文件处理、Http模拟请求、Excel导入导出、Json数据解析等。项目中最好由专人负责提供工具类,其它人直接使用,保证工具类的统一。

问题二、没有统一分页功能

由于没有统一分页功能,开发人员必需先写countSql,然后再写pageSql,十分不利于维护。推荐做法是通过数据库分页插件,拦截查询语句自动化分页(需要配合Page类实现),具体实现参照文后框架。

问题三、日志打印系列问题

日志打印有好几个问题,列举如下:

1、没有通过slf4j接口打印日志

通过slf4j日志门面实现与具体的日志框架解耦,方便以后更换日志框架,比如从log4j切换到logback。如果日志打印是基于slf4j接口的,只需要更换日志jar包即可,是不是很cool。

2、日志打印级别过低问题

有的项目中通篇打的debug级别日志,这完全是乱来。对于重要信息或异常信息,一定要打error级别日志。一般项目在上线后,可能会调高日志打印级别为warn或error级别,因此重要信息一定要打error日志,以方便线上定位问题。

3、没有日志拦截器功能

一般在请求刚进入action时,需要打印日志。不好的方式是在每个action里编写打印日志代码。正确的方法是通过日志拦截器,统一进行日志打印。另对于service层需要打印日志时,可通过AOP方式统一拦截处理。通过上面两种方式,实现了日志打印和业务代码之间的解耦,可做到随时取消或修改日志打印。

问题四、关于异常设计

一种情况是没有自定义异常;另一种情况是过度使用异常,甚至于在每个dao或service方法上都声明抛出异常。异常应该在确实会发生异常的情况下才使用

对于自定义异常,建议继承自RuntimeException(即运行时异常),而不是继承Exception(即受检异常)。当方法抛出受检异常时,意味着调用方必须进行处理,或继续往上抛出。只有在确认某个异常能被开发人员处理时,才能声明抛出受检异常,除此之外,都应该抛出非受检异常,此时需要在方法DOC上说明可能抛出的异常信息。

问题五、没有访问权限设计

没有统一的访问权限设计,导致在程序中硬编码方式进行权限管理。特别对于后台系统,更需要一种细粒度和可配置的角色权限管理,推荐使用shiro框架进行统一认证和授权管理,当然也可选择 Spring Security,相对来说shiro简单,易上手。

问题六、没有代码生成工具

对于高效开发来说,代码生成工具必不可少。想想你正在使用mybatis框架,手动拼写sql将是一个痛苦的过程,此时就更有必要使用一些自动化的工具。推荐使用rapid-generator(Java代码生成器)或者IDE插件进行代码生成。

问题七、没有命名规范

由于没有规范,每个开发人员各自为战,导致命名混乱不坑。这个其实牵涉到管理上的问题,最好是制定好项目的命名规范,包括包结构、目录结构、文件名甚至于方法和变量名等一系列规范。

结束语

搭建开发框架是每个项目的第一步,也是非常关键的一步,一定要慎之又慎。好的设计能够起到事半功倍的效果,相反,不好的设计只会拖住开发者的脚步,延缓项目进度。

最后希望每个框架设计人员能够脚踏实地,以解决开发者痛点为已任,一步步改进设计。没有最好,只有更好,唯有不满足现状,方能进步

后台开发框架项目地址https://github.com/tangqian/jeesite

说明:该系统为精简版的JeeSite系统,只保留用户、角色、权限部分,去掉了CMS及流程部分