jOOQ比起MyBatis的便利之处

单位新项目用了新的持久化框架,叫jOOQ

用惯MyBatis的我一开始有些不适应,但是写过新项目之后,回去维护老项目,确实感觉基于XML的MyBatis编码效率低下。

首先,两边同样有自动生成代码的工具。
MyBatis生成的有Dao层和pojo,需要配置生成哪张表的pojo,(随口一提,公司的生成器配置可能有bug,生成XML时并不会清空之前的XML内容,需要手动清空/删除一次。)

生成的dao提供的方法有:

  • 根据主键获取
  • 插入
  • 可选插入(pojo对应字段不是null的时候才设值)
  • 更新
  • 可选更新(不是null的时候才更新值)
  • 逻辑删除

pojo则是数据库的各字段以及get/set方法,值得一提的是每个字段上会有DDL里的列备注,但是会充斥大量空行,阅读起来较为困难。

而jOOQ的代码生成器一次性生成所有表的dao层和pojo。dao层全部继承了一个叫DAOImpl的抽象类,提供了以下方法:

  • 新增单个pojo
  • 新增多个pojo(可变参数)
  • 新增多个pojo(集合)
  • 用以上三种方式更新、物理删除pojo
  • 根据主键判断是否存在
  • 获取表中记录总数
  • 获取整张表
  • 利用Java8的Optional来获取(大概是允许参数为null?)
  • 获取主键

还有一些看不太懂的方法:

  • private /* non-final */ Condition equal(Field<?>[] pk, Collection<T> ids)
  • private /* non-final */ List<R> records(Collection<P> objects, boolean forUpdate)
  • private /* non-final */ RecordListenerProvider[] providers(final RecordListenerProvider[] providers, final Object object)

而每个表不同的dao实现类也有各自的方法:

  • 根据主键(项目里就是id)获取
  • 根据多个id获取多个记录(可变参数)
  • 根据每个唯一索引获取记录
  • 根据每个列获取多条记录,也支持可变参数

比MyBatis丰富很多,唯一的缺陷是没有自动生成的逻辑删除方法,初次维护项目很容易根据直觉使用delete方法,需要自动生成Service层代码进行封装。

生成的pojo则兼容了JPA的注解(目前没有在项目里用上),这方面有点hibernate的画风? 较MyBatis生成的pojo要紧凑很多,少了很多无谓的空行。
当业务变化,建立新表,jOOQ生成的代码可以快速满足大部分业务。

说完自动生成的代码,接下来谈谈编写业务代码的复杂度。
贴一段使用MyBatis的传统项目里的单表分页查询代码:

首先需要编写XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="对应的dao接口类名">
<select id="selectByPageQuery"
resultMap="自动生成的Mapper里的ResultMap">
select * from 表
where is_deleted = 0
order by id desc
LIMIT #{itemIndex}, #{pageSize}
</select>

<select id="countByPageQuery" resultType="java.lang.Integer">
select count(*) from 表
where is_deleted = 0
LIMIT #{itemIndex}, #{pageSize}
</select>
</mapper>

由于多个Mapper需要公用LIMIT #{itemIndex}, #{pageSize} 这段语句(还有一些权限控制的语句),事实上代码是这样的:

1
<include refid="com.***.dao.mapper.fragment.PageMapper.pageLimit"/>

所以无法使用注解方式的SQL。
接下来是接口:

1
2
3
4
public interface 表SimpleSelectDao {
int countByPageQuery(PageQuery query);
List<自动生成的实体类> selectByPageQuery(PageQuery query);
}

其中PageQuery是公司封装的分页类,不贴代码了,可以从SQL里看出用到的字段。
然后再在Service层使用:

1
2
3
4
5
...
int count = parcelTransferRecordSimpleSelectDao.countByPageQuery(query);
query.setItemTotal(count);
List<TxParcelTransferRecord> records = parcelTransferRecordSimpleSelectDao.selectByPageQuery(query);
...

相对于基于XML的MyBatis,jOOQ在业务变动时编写代码的效率更高,同样是分页获取代码,只需要7行:

说明:
Pageable是Spring 5的分页相关类,专门解决从前端传参到数据库查询的分页问题,不需要自己实现分页框架了。
Tuple2是公司内部实现的数据结构,用来返回两个不同类型的数据。
SelectConditionStepdsl是jOOQ提供的类和对象,其中dsl的类是DSLContext,用于执行SQL,改写生成器后可以使用Spring注入。

1
2
3
4
5
6
7
8
9
public Tuple2<List<表实体类>, Integer> pageVoById(Pageable pageable) {
SelectConditionStep<表Record类> step = dsl.selectFrom(表的枚举)
.where(表的枚举.IS_DELETED.eq(false));
List<表实体类> list = step.orderBy(表的枚举.ID.desc())
.limit((int) pageable.getOffset(), pageable.getPageSize())
.fetchInto(表实体类.class);
int count = dsl.fetchCount(step);
return new Tuple2<>(list, count);
}

这段代码出现在Service层,用惯之后很符合人类直觉,发挥了SQL人类可读性好的优势。

jOOQ封装了sql,将所有的字段和SQL语句化为Java代码,当然也可以传入完整或者部分的sql执行,自由度很高,同时有Java强类型的优势加持,屏蔽掉了MyBatis的ResultMapper环节,每一步SQL执行虽然要使用一些复杂的类和对象(例如SelectConditionStep),对习惯使用强类型语言的java码狗来说很有“安全感”。

相比jOOQ,基于XML的MyBatis代码编写繁琐,手写SQL经常漏掉order by id desc(页面上刚刚新增的记录在最前面)和is_deleted = 0(其实这是低级失误),查找SQL需要跳转两次(这还是装了IDE插件的情况),在多表查询时还要编写复杂的ResultMap,体验确实差很多。

禁用Session的subjectDAO、subjectFactory和sessionManager注入安全管理器。" class="headerlink" title="将配置为禁用Session的subjectDAO、subjectFactory和sessionManager注入安全管理器。">将配置为禁用Session的subjectDAO、subjectFactory和sessionManager注入安全管理器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--<property name="realm" ref="userRealm"/>-->
<property name="subjectDAO" ref="subjectDAO"/>
<property name="subjectFactory" ref="subjectFactory"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>

<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>

最后构建一个迭代7次的SHA-512 Hash服务,用来在类里调用。
1
2
3
4
5
<!--构建一个Hash服务,在此指定叠加方式和采用的算法-->
<bean id="defaultHashService" class="org.apache.shiro.crypto.hash.DefaultHashService">
<property name="hashAlgorithmName" value="SHA-512"/>
<property name="hashIterations" value="7"/>
</bean>
前文提到的StatelessDefaultSubjectFactory:
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.qhs.blog.util.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
接下来就是编写具体Filter了。

先贴一段代码,逐步讲解作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.qhs.blog.util.shiro.filter;


import com.qhs.blog.serviceImpl.tokenServiceImpl;
import net.minidev.json.JSONObject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
* Created by QHS on 2017/5/31.
*/
public class captchaFilter extends AccessControlFilter {
@Autowired
private tokenServiceImpl tokenService;

JSONObject resp = new JSONObject();

@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
boolean flag = false;
HttpServletRequest req = WebUtils.toHttp(servletRequest);
String token = req.getParameter("token");
Map<String, Object> resultMap = tokenService.validToken(token);
switch ((String) resultMap.get("state")) {
case "EXPIRED":
resp.put("warning", "授权已过期");
break;
case "VALID":
JSONObject jo = (JSONObject) resultMap.get("data");
String value = (String) jo.get("value");
if (value.equals("captchaAuthed") && value.equals("getMailCode")) {
flag = true;
}
break;
case "INVALID":
resp.put("msg", "请输入验证码");
break;
}

return flag;
}

@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {

servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("application/json; charset=utf-8");
servletResponse.getWriter().write(resp.toJSONString());
return false;
}

}

这个Filter继承了AccessControllFilter,shiro有几个内置Filter,有的用于认证,有的用于鉴权。而在这里,我只需要一个验证码,没必要处理用户登录,因此直接用了AccessControllFilter。

继承了类就需要继承方法,AccessControllFilter带了isAccessAllowed和onAccessdenied方法。

前者如果return了True,就允许用户访问Filter保护下的url,如果return False,那就会进入后面的方法,如果后面的方法Return True也允许用户访问(意义不明啊)。

因此我在类里定义变量JSONObject,在isAccessAllowed方法中对传入Token进行验证,验证不通过则在JSONObject内写入相应的错误信息并返回false。

进入onAccessDenied方法,在方法体内将JSONObject写入Servlet流输出到网页上,这样就完成了一个基本的无状态应用集成。
(其实我的做法和手写Filter差不多,只是引入shiro作为练手,和后续功能扩展。)

2.普通Web应用中使用shiro

分析思路:

现在在单位做的项目,计划中是一个由服务端渲染JSP的项目,因此可以使用Session和Cookie。

这时候引入shiro,就可以使用realm的相关特性,将userService注入realm中,由shiro接管用户认证、授权的成功/失败后的场景。可以说是充分发挥了功能。
Web.xml内容不变,Spring配置中去掉那些关于Session的设置,并且也不用写StatelessDefaultSubjectFactory。

具体写法:

修改XML配置
1
2
3
4
5
6
7
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm"/>
</bean>

<bean id="userRealm" class="com.shengting.store.util.shiro.realm.userRealm">
<property name="cachingEnabled" value="false"/>
</bean>
新建userRealm

由于这个Realm类需要同时完成认证/鉴权工作(认证(Authc)指的是判断用户是否能登录,而鉴权(Authz)指的是用户有没有权限执行某项操作。),我们让它继承AuthorizingRealm类。

继承了类就要继承方法,我们的类看起来是这样子的:

1
2
3
4
5
6
7
8
9
10
public class userRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}

后面的大体思路,就是调用userMapper在数据库中查找信息(也可以使用封装好的userService)
由于工作进度问题,Controller还没有写完,暂时先更新到这里;w;