5.Map + Wrapper
在Guns框架中,Controller层中采用了 map + wrapper 方式进行结果数据返回。利用wrapper类对返回结果进行包装,使得返回的前端数据更加灵活。
Wrapper的共同父类为BaseControllerWrapper,这是一个抽象类,需要子类去继承,并实现其内部的封装方法。
public abstract class BaseControllerWarpper {
public Object obj = null;
public BaseControllerWarpper(Object obj) {
this.obj = obj;
}
@SuppressWarnings("unchecked")
public Object warp() {
//包装方法中指出了要对传入Object进行类别判断
if (this.obj instanceof List) {
List<Map<String, Object>> list = (List<Map<String, Object>>) this.obj;
for (Map<String, Object> map : list) {
warpTheMap(map);
}
return list;
} else if (this.obj instanceof Map) {
Map<String, Object> map = (Map<String, Object>) this.obj;
warpTheMap(map);
return map;
} else {
return this.obj;
}
}
//子类需要去继承并实现的封装方法
protected abstract void warpTheMap(Map<String, Object> map);
}
这里的Controller层数据封装,是根据前端需要的数据进行的封装。在这里还有一个概念:VO层(表现层对象)。
需求来源就是,前端展示时,会有很多的变更,Controller层所返回的数据中需要进行对应前端的数据处理后返回。
看了一篇关于关于Java中各种对象的分析,这里整理总结下:
PO:持久对象(persistent object),对应ORM框架中的entity。PO的每个属性都是对应着数据库表中的某个字段。
即一个PO类对应一张表,一个PO实例对应表中的一行数据。PO实例是通过insert方法在数据库表中创建,delete方法在数据库表中删除。生命周期和数据库密切相关,准确的讲是和数据库连接的生命周期紧密相关。
VO:视图对象(view object),主要对应展示页面所显示的数据对象,用过VO对象来封装整个界面所需要展示的对象数据。
BO:业务对象(bussiness object),封装业务逻辑的Java对象,通过调用DAO的方法,结合VO和PO进行业务操作。
DTO:数据传输对象(data tranfer object),是一种设计模式之间传输数据的软件应用系统。
数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。
POJO:简单Java对象(plain ordinary java object),通指没有使用Entity Beans的普通java对象,可以把POJO作为支持业务逻辑的协助类。POJO持久化后就是PO。
所以说,Guns框架中所采用的 map + wrapper 返回前端所需数据,就是在后台进行了VO对象的包装。使之对应前端数据展示更加灵活多变。
这里我们可以归纳下框架中的数据展示过程:
Controller层:
//调用service层方法进行数据查询后返回List<Map<String, Object>>/Map<String, Object>类型数据集合。
List<Map<String, Object>> results = xxxDao.selectXxx(xxx,xxx);
//XxxWrapper为继承了BaseControllerWrapper的子类,并实现了其父类的warpTheMap方法。
//如下就是:创建新的Wrapper对象并调用包装方法,对返回结果进行包装。
return new XxxWrapper(results).warp();
这里需要注意的是:Wrapper所接收的参数类型只能是List<Map<String, Object>>/Map<String, Object>类型。
如果不是以上两种类型其包装方法wrap(),便会返回传入对象,不进行包装。即:包装无效。
Map<String, Object> key-value形式存储对象,List<Map<String, Object>> 将map统一放入list中存储。
针对单一的JavaBean对象,Guns框架还提供了JavaBean的工具类,可以采用工具类对普通的JavaBean对象进行封装。
BeanKit.java:Bean工具类
BeanKit.beanToMap():可以将进行对象转Map的操作,返回Map(String, Object)
例:根据Id查询订单信息,返回后进行普通PO对象和经过BeanKit工具类加工后的Bean对象的比对。
public class BeanKitTest extends BaseJunit{
@Autowired
private MyOrderMapper myOrderMapper;
@Test
public void test(){
MyOrder order = myOrderMapper.selectById(1);
Map<String, Object> map = BeanKit.beanToMap(order);
System.out.println(order + "\n" + map);
}
}
打印结果:
MyOrder{id=1, user=张三, place=北京海淀, goods=双汇火腿肠, createtime=Sat May 19 11:57:07 CST 2018}
{createtime=Sat May 19 11:57:07 CST 2018, goods=双汇火腿肠, id=1, place=北京海淀, user=张三}
可以看出,返回的常规PO对象order经过BeanKit加工后,构建了key-value这一形式的对象。
踩坑记录:
一开始构建Junit测试类的时候,代码一直提示空指针异常,debug后发现myOrderMapper值为null,嗯?spring容器中没有这个bean?
或者说是这里的@Autowired注入无效,这个时候才反应过来。Junit测试中的环境并不是spring容器,对比其他测试类发现:extends BaseJunit。
打开BaseJunit后发现:
@RunWith(SpringRunner.class) //运行器,使得当前测试运行于spring环境
@SpringBootTest(classes = GunsApplication.class) //springboot框架下的集成Junit单元测试
改正后,测试通过。
在发现上述问题之前,本人一度觉得是mapper映射的问题,通过mp代码生成器所生成的mapper和接口都是空文件,全部继承了BaseMapper。
在BaseMapper中定义了很多查询方法。也就是Mybatis-plus已经帮我们实现了基本的CURD操作。预留出的mapper文件是进行拓展和复杂查询的。
这里也构建了一个查询mapper。
MyOrder getOrderById(Integer orderId); //接口方法声明
mapper文件:
<?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="com.stylefeng.guns.common.persistence.dao.MyOrderMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.stylefeng.guns.common.persistence.model.MyOrder">
<id column="id" property="id" />
<result column="user" property="user" />
<result column="place" property="place" />
<result column="goods" property="goods" />
<result column="createtime" property="createtime" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, user, place, goods, createtime
</sql>
<select id="getOrderById" parameterType="java.lang.Integer" resultType="com.stylefeng.guns.common.persistence.model.MyOrder">
SELECT <include refid="Base_Column_List"/>
FROM my_order where id = #{id}
</select>
</mapper>
这里需要说明两点:
1.通用查询结果列:Base_Column_List,是可以自定义的。
2.<select>中where条件中的#{id},对应Model中的MyOrder。
/**
* 主键id
*/
@TableId(value="id", type= IdType.AUTO)
private Integer id;
最后测试下自己写的mapper,测试也通过了。
MyOrder{id=1, user=张三, place=北京海淀, goods=双汇火腿肠, createtime=Sat May 19 11:57:07 CST 2018}
{createtime=Sat May 19 11:57:07 CST 2018, goods=双汇火腿肠, id=1, place=北京海淀, user=张三}
6.日志系统
1.Guns框架中日志系统的使用步骤
2.日志系统的原理(初步了解)
3.自定义注解
日志系统的使用步骤:
1.1 在需要添加日志的业务逻辑方法前添加@Bussinesslog注解,注意要加载Controller层的方法上。
这里的@Bussinesslog为自定义注解。 -- 后面会对自定义注解的使用进行学习。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BussinessLog {
/**
* 业务的名称,例如:"修改菜单"
*/
String value() default "";
/**
* 被修改的实体的唯一标识,例如:菜单实体的唯一标识为"id"
*/
String key() default "id";
/**
* 字典(用于查找key的中文名称和字段的中文名称)
*/
String dict() default "SystemDict";
}
1.2 根据所需自定义字典。
字典的概念,通俗点理解就是字段转换。便于用户理解而设立的字段对应的解释含义。
1.3 日志保存变更:在业务逻辑涉及修改操作时,日志需要实现对修改前数据的暂存。
对系统的操作记录日志,有利于后期维护和检查。日志分为两类:一种仅仅记录操作日志即可,另一种就是要进行修改数据操作,此时日志需要实现对原数据和修改后数据的比对。
日志系统原理:AOP – 面向切面编程
这一部分很具备的研究的价值,这里就记录下。以下理解可能会有偏差,还望读者指正,谢谢。
AOP基本概念:在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限控制等方面。
1. Aspect(切面):通常是一个类,里面可以定义切入点和通知。
2. JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用。
3. Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around。
4. Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式。
5. AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
原理:切面类坐标:LogAop.java
这里指出需要注意的地方:
1. 切面类注解和组件注解:需要声明该类属于切面类,并将其注册到spring容器中。
@Aspect
@Component
public class LogAop {
//...
}
2. 切点:声明连接点,在该自定义注解下需要就行通知声明。
@Pointcut(value = "@annotation(com.stylefeng.guns.common.annotion.BussinessLog)")
public void cutService() {}
3. 通知:通知方式有很多种,这里采用了环绕通知(round)。
环绕通知可以拦截对目标方法的调用,在目标方法的前后各执行一段逻辑。和前置和后置通知不同的是:环绕通知能获取到目标方法的返回值,且能够改变这个返回值。
@Around("cutService()")
public Object recordSysLog(ProceedingJoinPoint point) throws Throwable {
//执行业务逻辑
Object result = point.proceed();
try {
handle(point);
} catch (Exception e) {
log.error("日志记录出错!", e);
}
return result;
}
4. handle()中进行了获取切面类相关参数,拦截方法相应参数,若业务逻辑中含有修改数据逻辑需要特别处理。这里就修改数据逻辑所产生的日志生成策略进行分析。
4.1 判断是否是数据修改或编辑业务逻辑。
判断依据是被@Bussinesslog修饰的方法中,@Bussinesslog参数value中是否含有修改/编辑子串。
if (bussinessName.indexOf("修改") != -1 || bussinessName.indexOf("编辑") != -1)
4.2 通过判断表明当前被@Bussinesslog修饰的方法含有修改/编辑逻辑。
//LogObjectHolder.me()方法表明在用户选择编辑操作时,弹出框中所显示的数据(原始数据)被暂存了。
//这里调用get()方法进行获取暂存数据,
Object obj1 = LogObjectHolder.me().get();
//当用户对数据进行修改操作后,点击提交按钮后,获取发送的request中data所携带的数据。用到了HttpKit工具类。
Map<String, String> obj2 = HttpKit.getRequestParameters();
/*比较obj1和obj2的异同,并返回日志描述msg。
* 这里对参数进行说明:
* dictclass:字典,用来字段转换,将不易理解的字段进行转换。
* key:@Bussinesslog注解参数,用来标识被修改实体的唯一标识。
*/
msg = Contrast.contrastObj(dictClass, key, obj1, obj2);
4.3 采用异步操作进行日志生成。
LogManager.me().executeLog(LogTaskFactory.bussinessLog(user.getId(), bussinessName, className, methodName, msg));
5. 这里需要指明一个地方,暂存对象(LogObjectHolder.me())保存的对象,应该和DTO数据传输对象在字段上保持一致。
原因:当修改业务完成后提交时,发送请求后,比对的两个对象obj1为暂存对象,obj2从请求data中获取的DTO,若不一致会影响比对进程,这里便用到了上文中提到的数据传输对象DTO。
例:这里的user就是DTO对象,用于和暂存对象进行比对。
@RequestMapping("/edit")
@BussinessLog(value = "修改管理员", key = "account", dict = Dict.UserDict)
@ResponseBody
public Tip edit(@Valid UserDto user, BindingResult result) throws NoPermissionException {
//...
}
自定义注解:
以@Bussinesslog注解为例子。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BussinessLog {
//...
}
1. 什么是注解:Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。
Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
2. 元数据(metadata):关于数据的数据,可以用来创建文档,跟踪代码的依赖性,执行编译时格式检查,代替已有的配置文件。
3. 根据使用方法和用途对注解分类:系统注解,元注解,自定义注解。
系统注解:
1. @Override:用于重写父类的方法 或者是写接口实现类时用到该注解。
2. @Deprecated:用于表示该方法是一个过期的方法。
3. @suppressWarnings:表示该方法在编译时自动忽略警告。
元注解:
1. @Target:用于描述注解的使用范围。
ElementType取值:
1. CONSTRUCTOR:用于描述构造器
2. FIELD:用于描述域
3. LOCAL_VARIABLE:用于描述局部变量
4. METHOD:用于描述方法
5. PACKAGE:用于描述包
6. PARAMETER:用于描述参数
7. TYPE:用于描述类、接口(包括注解类型) 或enum声明
例:@Target({ElementType.METHOD}) 用于描述方法。
2. @Retention:用于描述注解的生命周期。
RetentionPoicy取值
1. SOURCE:在源文件中有效
2. CLASS:在class文件中有效
3. RUNTIME:在运行时有效
例:@Retention(RetentionPolicy.RUNTIME) 运行时有效
3. @Documented:声明需要加入JavaDoc。
4. @Inhrited:表明了被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
不太理解这一个元注解,查阅资料后,写了例子证明上述说法。
首先我们自定义一个注解:具体自定义注解内容会在下面指出。
//自定义注解:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String value() default "";
}
//测试类
public class Test1 {
@TestAnnotation
class A{}
class B extends A{}
public static void main(String[] args) {
System.out.println(B.class.isAnnotationPresent(TestAnnotation.class));
}
}
测试结果:true 证实上述说法
自定义注解:@interface用来声明一个自定义注解,自动继承java.lang.annotation.Annotation接口。
自定义注解格式:public @interface 注解名{注解体}
自定义注解体格式:基本数据类型/String/Class/enum/Annotation
例:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String value() default "";![](https://i.imgur.com/k3pK81p.jpg)
}
注意:自定义注解访问修饰符只有public和default。
Java Annotation思维导图:https://i.imgur.com/GkmI9D6.jpg
8/6/2018 6:26:54 PM