后台网站包括springboot项目工程和vue项目工程, 先来建立springboot工程. 可以参考我之前写的这篇: springboot入门
pom文件
注意工程使用的是mysql数据库, 所以要将pom.xml 文件改成:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<version>4.0.0</version>
<groupId>org.diego</groupId>
<artifactId>diegomall</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<build>
<plugins>
<!--spring boot打包的话需要指定一个唯一的入口 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 指定该Main Class为全局的唯一入口 -->
<mainClass>org.diego.mall.StartApp</mainClass>
<layout>ZIP</layout>
<profiles>
<profile>dev</profile>
<profile>prod</profile>
</profiles>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal><!--可以把依赖的包都打包到生成的Jar包中 -->
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- 打包时跳过测试 tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>
</project>
工程结构解释
工程的包图如下所示
image.png
dev目录: 存放一些开发过程中使用的代码
config: 存放springboot可能的配置
controller: 存放springmvc 的controller
dao目录: 存放dao类
pojo目录: 存放pojo类,一个pojo代表着一张数据表
service类: 存放service, service的方法都开启着事务管理
utils: 存放一些通用的工具方法类
vo: 存放值对象, 这些对象通常会在业务层传输来往.
这是一个经典的mvc工程, 做java的同学都很熟悉, 就不多解释了.
mybatis增删改查功能的封装
众所周知, java项目很容易引起类膨胀, 其实二八法则同样适合于软件项目. 80%的功能都是重复性的, 没必要增加太多的类来实现它们. 所以我做了mybatis通用功能的封装, 它们包括 CommonDao, CommonDaoDaoMapper.xml, ISingleTableService 和 AbstractSingleTableService 4个类. 我没有一下子把项目的代码都copy过来, 我想随着这个博客的进程而逐步把代码加进来, 这样也容易帮助自己思考优化.
封装功能的实现
CommonDao
package org.diego.mall.dao;
import java.util.Map;
import java.util.TreeMap;
import org.apache.ibatis.annotations.Param;
/**
* @author cys
*/
public interface CommonDao {
// 插入一条数据
Long insertOne(@Param("tableName")String tableName, @Param("dataMap")TreeMap<String, Object> dataMap);
// 查找字段第一条数据
Map getFirst(@Param("tableName")String tableName, @Param("fieldName")String fieldName, @Param("fieldValue")Object fieldValue);
// 根据sql语句查找第一条数据
Map getFirstBySql(@Param("tableName")String tableName, @Param("sqlFilter")String sqlFilter);
}
CommonDaoMapper.xml
<?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="org.diego.mall.dao.CommonDao">
<!-- ============================ 插入单条数据 -->
<insert id="insertOne" flushCache="true" useGeneratedKeys="true" keyProperty="dataMap.id">
insert into ${tableName}
<foreach collection="dataMap.keys" separator="," open="(" close=")" index="index" item="item">
`${item}`
</foreach>
values
<foreach collection="dataMap.values" separator="," open="(" close=")" index="index" item="item">
#{item}
</foreach>
</insert>
<!-- ============================ 返回符合字段条件的第一条数据 -->
<select id="getFirst" resultType="HashMap">
select * from ${tableName} where `${fieldName}` = #{fieldValue} limit 0,1
</select>
<!-- ============================ 返回符合字段条件的第一条数据 -->
<select id="getFirstBySql" resultType="HashMap">
select * from ${tableName} where 1=1
<if test="sqlFilter != null">
${sqlFilter}
</if>
limit 0,1
</select>
</mapper>
ISingleTableService
package org.diego.mall.service;
public interface ISingleTableService<T> {
// 根据字段 获取对象, 如果有多个返回第一个
T getFirst(String name, Object value);
// 插入一条数据
Long insertOne(Long userId,T entity, String[] inputAvoidFields) throws Exception;
// 根据sql 获取对象, 如果有多个返回第一个
T getFirstBySql(String sqlFilter);
}
AbstractSingleTableService
package org.diego.mall.service.impl;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.diego.mall.dao.CommonDao;
import org.diego.mall.service.ISingleTableService;
import org.diego.mall.utils.RefUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
/**
*
* 这个基类的目的是减少大量重复的增删改查表的操作, 让mapper.xml真正保存有用的业务sql
*
* 1) 对于那些单表增删改查的业务模块, 继承这个类之后基本上不需要写新建/更新/单表查询的service代码了.
* 2) 默认实现了一个无关联表的分页查询. 如果有关联的分页, 还是要自己实现.
*
* @author cys
*
*/
public class AbstractSingleTableService<T> implements ISingleTableService<T>, InitializingBean {
public static final String CREATE_TIME = "createTime";
public static final String UPDATE_TIME = "updateTime";
public static final String CREATE_USER_ID = "createUserId";
public static final String UPDATE_USER_ID = "updateUserId";
private Logger logger = LoggerFactory.getLogger(AbstractSingleTableService.class);
protected String tableName = null;
protected Class<T> clazz = null;
protected List<String> defaultUpdateAvoidFields; // 默认在新建/修改时忽略的字段(如外键字段)
@Autowired
CommonDao commonDao;
@Override
public void afterPropertiesSet() throws Exception {
clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
// System.out.print("=============== simple name: " + clazz.getSimpleName());
// System.out.print("=============== con name: " + clazz.getCanonicalName());
this.tableName = clazz.getSimpleName();
// 不新建/修改某些字段
this.defaultUpdateAvoidFields = new ArrayList<>();
Map<String,Method> fieldMap = RefUtils.findFieldWithGetter(clazz.newInstance());
for(String name : fieldMap.keySet()) {
if(name.startsWith("ex") || name.startsWith("EX")) {
logger.debug("== ex field: " + name);
this.defaultUpdateAvoidFields.add(name);
}
}
}
@Override
public T getFirst(String fieldName, Object fieldValue) {
Map data = commonDao.getFirst(this.tableName, fieldName, fieldValue);
if(data == null)return null;
return (T) RefUtils.copyFromMapToObject(data, this.clazz, false, null);
}
@Override
public Long insertOne(Long userId,T entity, String[] inputAvoidFields) throws Exception {
// 自动将ex字段排除
String[] avoidFields = RefUtils.checkAndMakeProperAvoidFields(this.defaultUpdateAvoidFields, inputAvoidFields);
this.setCreateInfoIfExist(userId, entity);
TreeMap<String, Object> dataMap = RefUtils.copyFieldsToMap(entity, false, avoidFields, null);
commonDao.insertOne(this.tableName, dataMap);
if(dataMap.get("id") == null) return null;
Long id = Long.parseLong(dataMap.get("id").toString());
return id;
}
private void setCreateInfoIfExist(Long userId, T entity) {
try {
Map<String, Method> dmap = RefUtils.findFieldWithSetter(entity);
if (userId !=null && dmap.containsKey(CREATE_USER_ID)) {
Method m = dmap.get(CREATE_USER_ID);
m.invoke(entity, userId);
}
if (dmap.containsKey(CREATE_TIME)) {
Method m = dmap.get(CREATE_TIME);
m.invoke(entity, new Date());
}
} catch (Exception e) {
logger.error("", e);
}
}
private void setUpdateInfoIfExist(Long userId, T entity) {
try {
Map<String, Method> dmap = RefUtils.findFieldWithSetter(entity);
if (dmap.containsKey(UPDATE_USER_ID)) {
Method m = dmap.get(UPDATE_USER_ID);
m.invoke(entity, userId);
}
if (dmap.containsKey(UPDATE_TIME)) {
Method m = dmap.get(UPDATE_TIME);
m.invoke(entity, new Date());
}
} catch (Exception e) {
logger.error("", e);
}
}
@Override
public T getFirstBySql(String sqlFilter) {
Map data = commonDao.getFirstBySql(this.tableName, sqlFilter);
if(data == null)return null;
return (T) RefUtils.copyFromMapToObject(data, this.clazz, false, null);
}
}
通过这4个类, 就能省去大部分单表数据的增删改查了.
这种做法的缺点
凡事有利弊. 类膨胀的坏处是项目文件变多, 但哪个模块的方法在哪个类, 需求变更以后方法该加到哪里都一目了然. 上述这种单表的封装类, 无法解决多表联合查询, 无法实现复杂的业务. 如果有新人加入到新的项目, 他很容易把代价加到不应该加的地方.
用了这种办法虽然省了重复的工作量, 但必须有很好的项目文档规范作为约束, 这算是这种做法的缺点吧.
网友评论