在java程序员的平时工作中除了会遇到普通的集合类型,也免不了会遇到树形结构。这种数据结构相比简单的List、Set、Map相对来说会更加复杂一些,jdk中也没有对应的数据类型可以表示。所以开发者们在面对这一类的数据结构的时候总是需要自己来构建。例如下面这个例子。
例1:权限与菜单。我们可以构想这样一个场景。在一个OA系统中,不同权限/角色的用户登录需要在前端html中展现不同的菜单项目。在大多数情况下我们的菜单栏一般会设计成树形结构。在很多的前端框架中也有这样的模块。例如Easyui 中的树形菜单,可见下图。那么对应的我们后台中对于菜单的处理同样需要构建成为一个树形结构。
后台数据库的设计。保存表名为tb_menu;
插入几条用于测试的数据。
下面开始编码,我们先写一个数据库工具类。我用的是mybaits,当然也可以用别的orm框架,如hibernate、spring-jdbc ...等等。
public class MybatisUtil {
private static SqlSessionFactory sessionFactory;
static {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
InputStream fis = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml");
sessionFactory = sqlSessionFactoryBuilder.build(fis);
}
public static SqlSession getSqlSession() {
return sessionFactory.openSession();
}
}
创建java bean
public class Menu {
private Integer id;
private Integer pid;
private String name;
private String url;
private List<Menu> childrenList;
public Menu() {
}
public Menu(Integer id, Integer pid, String name, String url, List<Menu> childrenList) {
this.id = id;
this.pid = pid;
this.name = name;
this.url = url;
this.childrenList = childrenList;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getPid() {
return pid;
}
public void setPid(Integer pid) {
this.pid = pid;
}
public List<Menu> getChildrenList() {
return childrenList;
}
public void setChildrenList(List<Menu> childrenList) {
this.childrenList = childrenList;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
mybatis-config.xml & MenuMapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="url" value="jdbc:mysql://localhost:3306/local_db"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="driver" value="com.mysql.jdbc.Driver"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.itsu.mapper.MenuMapper"/>
</mappers>
</configuration>
public interface MenuMapper {
@Select("select * from tb_menu")
List<Menu> getAllMenu();
}
好了现在环境准备好了,我们来构建一个最简单最基本的树形结构。算法比较简单,稍微有点java基础的小伙伴应该都可以看到懂。利用了一个很基础的递归算法实现。然后运行看看结果。
@Test
public void testMenu() {
SqlSession sqlSession = MybatisUtil.getSqlSession();
MenuMapper mapper = sqlSession.getMapper(MenuMapper.class);
//查询出数据库中存储的所有menu
List<Menu> menus = mapper.getAllMenu();
List<Menu> treeList = transferToTree(menus, new ArrayList<Menu>());
System.out.println(JSON.toJSONString(treeList));
}
/**
* 当pid为null时,也就是当前menu为一级菜单时的处理
* @param source
* @param treeList
* @return
*/
private List<Menu> transferToTree(List<Menu> source, ArrayList<Menu> treeList) {
for (Menu menu : source) {
if (menu.getPid() == null) {
Menu m = new Menu();
m.setId(menu.getId());
m.setName(menu.getName());
m.setPid(menu.getPid());
m.setUrl(menu.getUrl());
m.setChildrenList(transferToTree(source, m.getId()));
treeList.add(m);
}
}
return treeList;
}
/**
* 当传入的pid和当前menu的pid相等的时候的处理
* 通过递归算法轮询整个source list
* @param source
* @param pid
* @return
*/
private List<Menu> transferToTree(List<Menu> source, Integer pid) {
List<Menu> childrenList = new ArrayList<>();
for (Menu menu : source) {
if (menu.getPid() != null && menu.getPid().equals(pid)) {
Menu m = new Menu();
m.setId(menu.getId());
m.setName(menu.getName());
m.setPid(menu.getPid());
m.setUrl(menu.getUrl());
m.setChildrenList(transferToTree(source, m.getId()));
childrenList.add(m);
}
}
return childrenList;
}
运行结果:
我们用json格式化工具看一看,的确完成了这个基本的树形结构的构建。
[
{
"childrenList": [
{
"childrenList": [],
"id": 4,
"name": "新增用户",
"pid": 1,
"url": "user/adduser"
},
{
"childrenList": [],
"id": 5,
"name": "删除用户",
"pid": 1,
"url": "user/deluser"
},
{
"childrenList": [],
"id": 6,
"name": "修改用户信息",
"pid": 1,
"url": "user/updateuser"
}
],
"id": 1,
"name": "用户管理",
"url": "user/admin"
},
{
"childrenList": [
{
"childrenList": [],
"id": 7,
"name": "新增商品",
"pid": 2,
"url": "prod/addprod"
},
{
"childrenList": [],
"id": 8,
"name": "删除商品",
"pid": 2,
"url": "prod/delprod"
}
],
"id": 2,
"name": "商品管理",
"url": "user/product"
},
{
"childrenList": [
{
"childrenList": [],
"id": 9,
"name": "新增职员",
"pid": 3,
"url": "stuff/addstuff"
},
{
"childrenList": [],
"id": 10,
"name": "删除职员",
"pid": 3,
"url": "stuff/delsutff"
}
],
"id": 3,
"name": "职员管理",
"url": "user/stuff"
}
]
到这里功能已经实现了。但是这里有一个遗留的问题,就是对于一个树形结构的构建,我们总是需要根据当前需要进行树形结构的实体类进行编码。通俗一点来说就是说我们的代码不能复用。我们需要一个能够通用的树形结构构建工具。这样子是最理想的。那么怎么实现呢,请接着往下看。
1.设计思路。
既然需要通用,也就是对于任意的java bean都可以适用。那么免不了的需要使用到反射。如果使用的反射,另一个问题就出现了。如何确定java bean中的哪一个属性为id?哪一个树形为pid?哪一个属性为children list?剩余的其他属性如何处理?这就需要在相关树形中做一个“标记”。我们先来回想一下一些第三方框架是怎么处理这种情况的呢?看看hibernate, 通过@Id的方式来确认主键,再看看mybait -plus 通过@tableId来标记主键,@tablename来标记java bean对应哪一个数据库表。那么这样就很明显了,jdk1.5以后就推出了注解功能。我们可以使用注解来标记java bean以及java bean中的属性。
2.编码过程
我们先创建几个注解。
定义这个类是需要转化为树形结构的类,并定于一个是否需要排序的属性。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TreeBean {
boolean sort() default false;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TreeId {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TreePid{
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TreeChildren {
}
然后我们来写一个工具类来处理,代码如下 :
package com.itsu.utils;
import com.alibaba.fastjson.JSON;
import com.itsu.annotation.TreeBean;
import com.itsu.annotation.TreeChildren;
import com.itsu.annotation.TreeId;
import com.itsu.annotation.TreePid;
import com.itsu.entity.Menu;
import org.slf4j.Logger;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* @author 苏犇
* @date 2019/7/27 16:38
*/
public class TreeUtil {
private static Field ID;
private static Field PID;
private static Field children;
private static List<Field> otherFields = new ArrayList<>();
private static boolean sort_flag = false;
private static Logger logger = LoggerUtil.getLogger(TreeUtil.class);
private TreeUtil() {
}
public static <T> List<T> transferToTree(List<T> source) throws IllegalAccessException, InstantiationException {
long beginTime = new Date().getTime();
if (!check(source)) {
logger.info("source list check not pass , source list will not transfer to a tree model");
return source;
}
init(source);
List<T> treeList = transfer(source, new ArrayList<>());
logger.info("get TreeList :" + JSON.toJSONStringWithDateFormat(treeList, "yyyy-MM-dd HH:mm:ss:SSS"));
long finishedTime = new Date().getTime();
logger.info("transfer to tree completed , use time {} ms", (finishedTime - beginTime));
return treeList;
}
private static <T> List<T> transfer(List<T> source, List<T> treeList) throws IllegalAccessException, InstantiationException {
for (T t : source) {
if (PID.get(t) == null) {
T obj = (T) t.getClass().newInstance();
ID.set(obj, ID.get(t));
PID.set(obj, PID.get(t));
invokeOthers(t, obj);
children.set(obj, transfer(source, ID.get(t)));
treeList.add(obj);
}
}
if (sort_flag) {
treeList.sort((x, y) -> {
int result = 0;
try {
result = ID.get(x).hashCode() - ID.get(y).hashCode();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
});
}
return treeList;
}
private static <T> List<T> transfer(List<T> source, Object pid) throws IllegalAccessException, InstantiationException {
List<T> childrenList = new ArrayList<>();
for (T t : source) {
if (PID.get(t) != null && PID.get(t).equals(pid)) {
T obj = (T) t.getClass().newInstance();
ID.set(obj, ID.get(t));
PID.set(obj, PID.get(t));
invokeOthers(t, obj);
children.set(obj, transfer(source, ID.get(t)));
childrenList.add(obj);
}
}
if (sort_flag) {
childrenList.sort((x, y) -> {
int result = 0;
try {
result = ID.get(x).hashCode() - ID.get(y).hashCode();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
});
}
return childrenList;
}
private static <T> void invokeOthers(T t, T obj) throws IllegalAccessException {
for (Field field : otherFields) {
field.set(obj, field.get(t));
}
}
private static <T> void init(List<T> source) {
logger.info("init start ...");
sort_flag = source.get(0).getClass().getAnnotation(TreeBean.class).sort();
Field[] fields = source.get(0).getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(TreeId.class)) {
ID = field;
ID.setAccessible(true);
continue;
}
if (field.isAnnotationPresent(TreePid.class)) {
PID = field;
PID.setAccessible(true);
continue;
}
if (field.isAnnotationPresent(TreeChildren.class)) {
children = field;
children.setAccessible(true);
continue;
}
field.setAccessible(true);
otherFields.add(field);
}
logger.info("init finished ...");
}
private static <T> boolean check(List<T> source) {
logger.info("check source begin ...");
if (CollectionUtils.isEmpty(source)) {
logger.warn("source list is empty ...");
return false;
}
T t = source.get(0);
if (t.getClass().getAnnotation(TreeBean.class) == null) {
logger.warn("source Bean not marked by @TreeBean");
return false;
}
List<Field> idFileds = new ArrayList<>();
List<Field> pidFileds = new ArrayList<>();
List<Field> childrenFields = new ArrayList<>();
Field[] fields = t.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(TreeId.class)) {
idFileds.add(field);
}
if (field.isAnnotationPresent(TreePid.class)) {
pidFileds.add(field);
}
if (field.isAnnotationPresent(TreeChildren.class)) {
childrenFields.add(field);
}
}
if (idFileds.size() != 1) {
logger.warn("source bean expire one Field marked by @TreeId , but found {} of it", idFileds.size());
return false;
}
if (pidFileds.size() != 1) {
logger.warn("source bean expire one Field marked by @TreePid , but found {} of it", pidFileds.size());
return false;
}
if (childrenFields.size() != 1) {
logger.warn("source bean expire one Field marked by @TreeChildren , but found {} of it", childrenFields.size());
return false;
}
logger.info("check finished ...");
return true;
}
}
简单解释,先找到对应的java bean是否有你定义的注解。如果有,则将这个属性保存下来。然后就是通过递归算法来实现构建。这个和之前的基本树形结构的算法一致。只不过是通过反射来取属性的get / set方法。新增的sort属性,只是用来判断构建出来的数组是否需要排序。如果需要排序,就通过id字段的hashcode 来排序。
那么我们的java bean也需要做一下改造。
@TreeBean(sort = true)
public class Menu {
@TreeId
private Integer id;
@TreePid
private Integer pid;
private String name;
private String url;
@TreeChildren
private List<Menu> childrenList;
我们来测试一下:
程序运行成功了,来检查一下结果,使用json格式化工具format一下:
[
{
"childrenList": [
{
"childrenList": [],
"id": 4,
"name": "新增用户",
"pid": 1,
"url": "user/adduser"
},
{
"childrenList": [],
"id": 5,
"name": "删除用户",
"pid": 1,
"url": "user/deluser"
},
{
"childrenList": [],
"id": 6,
"name": "修改用户信息",
"pid": 1,
"url": "user/updateuser"
}
],
"id": 1,
"name": "用户管理",
"url": "user/admin"
},
{
"childrenList": [
{
"childrenList": [],
"id": 7,
"name": "新增商品",
"pid": 2,
"url": "prod/addprod"
},
{
"childrenList": [],
"id": 8,
"name": "删除商品",
"pid": 2,
"url": "prod/delprod"
}
],
"id": 2,
"name": "商品管理",
"url": "user/product"
},
{
"childrenList": [
{
"childrenList": [],
"id": 9,
"name": "新增职员",
"pid": 3,
"url": "stuff/addstuff"
},
{
"childrenList": [],
"id": 10,
"name": "删除职员",
"pid": 3,
"url": "stuff/delsutff"
}
],
"id": 3,
"name": "职员管理",
"url": "user/stuff"
}
]
能看到,的确能看到所有的数据已经构建成了一个树形结构,并且各个属性也都是排好序的。
到这里,我们就完成了一个通用的树形结构构建器。当然还可以继续改造,比如加入排序的方式,是升序还是降序?通过哪一个树形进行排序?排序的具体规则是什么?(我这里写死了是通过ID 属性的hashcode进行排序,小伙伴们也可以自己去改造程序来定义)
总结与体会:
这一段程序说起来并不难,主要具备一定的java 开发水平的小伙伴们应该都能够写出来。重要的是这种编程思想,要能够举一反三,触类旁通。java 的技术栈很深,市面上出现的框架也很丰富。相比之下呢,我觉得java 基础反而更加重要。反射、注解、jvm方面的知识往往是比你精通多少框架更加重要。这就有点像无效小说,java 基础是内功,而各种各样的框架则是外功。只要内功基础牢固,外功自然不在话下。而且市面上的各种框架层出不穷,没过多久就会有新的框架出现取代旧的框架。多年前java开发者使用ssh开发项目,到后来大多使用ssm开发项目,再到现在”微服务“技术的崛起,springboot、springcloud的出现。这些无不印证了这一点。
(一点点个人的肤浅感受,大神勿喷。?)