美文网首页Java攻城狮Spring Boot
如何使用Spring Boot从0到1搭建一个Java后台(二)

如何使用Spring Boot从0到1搭建一个Java后台(二)

作者: 国士无双A | 来源:发表于2017-11-07 12:54 被阅读196次

    本篇文章将一步步地来实现一个完整的RESTful接口以及对应的单元测试与集成测试。

    数据库

    第一步,首先来创建一个部门表并插入几条数据,打开MySQLWorkbench,执行下面的sql语句:

    # 创建数据库ddn_hrm_db
    create database ddn_hrm_db;
    # 使用数据库ddn_hrm_db
    use ddn_hrm_db;
    # 创建表dept_inf
    create table dept_inf (
        id INT(11) not null auto_increment,
        name varchar(50) not null,
        remark varchar(300) default null,
        primary key (id)
    );
    
    # 插入几条数据
    insert into dept_inf(id, name, remark) values (1, '技术部', '技术部'), (2, '运营部', '运营部'), (3, '财务部', '财务部'), (4, '总公办', '总公办'), (5, '市场部', '市场部');
    

    用MySQLWorkbench查询下刚才插入进去的数据,验证下表建立的是否正确、插入数据是否正确,查询结果如下所示:

    spring-boot 1-1.png

    第二步,在Spring Boot中配置mybatis,配置数据库连接池(使用阿里的Druid),先打开pom.xml加入mybatis、druid所依赖的jar包,配置如下所示:

    <!-- Mybatis集成 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.30</version>
    </dependency>
    
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.1</version>
    </dependency>
    
    <!-- 阿里数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.4</version>
    </dependency>
    

    接着打开application.properties加入JDBC配置、druid配置、mybatis配置,配置如下所示:

    # JDBC配置
    spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/ddn_hrm_db?useUnicode=true&characterEncoding=utf8
    spring.datasource.druid.username=root
    spring.datasource.druid.password=
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    
    # 连接池配置
    spring.datasource.druid.initial-size=10
    spring.datasource.druid.max-active=50
    spring.datasource.druid.min-idle=10
    spring.datasource.druid.max-wait=60000
    spring.datasource.druid.time-between-eviction-runs-millis=60000
    spring.datasource.druid.min-evictable-idle-time-millis=300000
    
    # 监控配置
    # WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
    spring.datasource.druid.web-stat-filter.enabled=true
    spring.datasource.druid.web-stat-filter.url-pattern=/**
    spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
    spring.datasource.druid.web-stat-filter.session-stat-enable=true
    spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
    spring.datasource.druid.web-stat-filter.profile-enable=true
    
    # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
    spring.datasource.druid.stat-view-servlet.enabled=true
    spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
    spring.datasource.druid.stat-view-servlet.reset-enable=true
    spring.datasource.druid.stat-view-servlet.login-username=bruce
    spring.datasource.druid.stat-view-servlet.login-password=bruce2017
    
    # Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置
    spring.datasource.druid.aop-patterns=com.dodonew.service.*,com.dodonew.dao.*  # Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
    # 如果spring.datasource.druid.aop-patterns要代理的类没有定义interface请设置spring.aop.proxy-target-class=true
    
    # Filter配置
    spring.datasource.druid.filters=stat,wall
    
    # logging配置
    logging.level.org.mybatis.spring=debug
    # 显示SQL日志
    logging.level.com.dodonew.dao=debug
    
    # mybatis配置
    mybatis.configuration.cache-enabled=true
    mybatis.configuration.jdbc-type-for-null=null
    mybatis.configuration.call-setters-on-nulls=true
    

    在上面的配置中,显示SQL日志的配置要特别注意下,其中com.dodonew.dao是你mapper类所在的包,只有这样配置才能把sql日志给打印出来。

    第三步,建立Dept域对象、DeptDao类、DeptService类,如下所示:

    Dept域对象:

    public class Dept implements Serializable {
        private static final long serialVersionUID = -4243387151355500160L;
        private Integer id;
        private String departName;
        private String remark;
    
        public Dept(Integer id, String departName, String remark) {
            this.id = id;
            this.departName = departName;
            this.remark = remark;
        }
    
        public Dept(String departName, String remark) {
            this.departName = departName;
            this.remark = remark;
        }
    
        public Dept() {
    
        }
    
        public Integer getId() {
            return id;
        }
    
        public String getDepartName() {
            return departName;
        }
    
        public String getRemark() {
            return remark;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public void setDepartName(String departName) {
            this.departName = departName;
        }
    
        public void setRemark(String remark) {
            this.remark = remark;
        }
    
        @Override
        public String toString() {
            return "Dept{" +
                    "id=" + id +
                    ", departName='" + departName + '\'' +
                    ", remark='" + remark + '\'' +
                    '}';
        }
    }
    

    DeptDao、DeptDynaSqlProvider类:

    public interface DeptDao {
        /**
        * 查询所有部门
        * @return 所有部门
        */
        @Select("select * from " + HrmConstants.DEPTTABLE + " ")
        List<Dept> selectAllDept();
    
        /**
        * 根据id查询部门
        * @param id 部门id
        * @return 某个部门
        */
        @Select("select * from " + HrmConstants.DEPTTABLE + " where id = #{id}")
        Dept selectById(Integer id);
    
        /**
        * 根据id删除部门
        * @param id 部门id
        */
        @Delete("delete from " + HrmConstants.DEPTTABLE + " where id = #{id}")
        Integer deleteById(Integer id);
    
        /**
        * 查询总数量
        * @param params
        * @return 部门总数量
        */
        @SelectProvider(type = DeptDynaSqlProvider.class, method = "count")
        Integer count(Map<String, Object> params);
    
        /**
        * 分页动态查询
        * @param params
        * @return 部门列表
        */
        @SelectProvider(type = DeptDynaSqlProvider.class, method = "selectWithParams")
        List<Dept> selectByPage(Map<String, Object> params);
    
        /**
        * 动态插入部门
        * @param dept
        *
        * @SelectKey 注解的主要作用就是把当前插入对象的主键值,赋值给对应的id属性(id代表对应的主键)
        */
        @InsertProvider(type = DeptDynaSqlProvider.class, method = "insertDept")
        @SelectKey(statement = "SELECT LAST_INSERT_ID() AS id", keyProperty = "id", keyColumn = "id", before = false, resultType = Integer.class)
        Integer save(Dept dept);
    
        /**
        * 更新某个部门的信息
        * @param dept
        */
        @UpdateProvider(type = DeptDynaSqlProvider.class, method = "updateDept")
        Integer update(Dept dept);
    }
    
    public class DeptDynaSqlProvider {
        // 分页动态查询
        public String selectWithParams(final Map<String, Object> params) {
            String sql = new SQL() {
                {
                    SELECT("*");
                    FROM(HrmConstants.DEPTTABLE);
                    if (params.get("dept") != null) {
                        Dept dept = (Dept) params.get("dept");
                        if (dept.getDepartName() != null && !"".equals(dept.getDepartName())) {
                        WHERE(" departname like concat ('%', #{dept.departName}, '%') ");
                        }
                    }
                }
            }.toString();
    
            if (params.get("pageModel") != null) {
                sql += " limit #{pageModel.firstLimitParam}, #{pageModel.pageSize}";
            }
    
            return sql;
        }
    
        // 动态查询总数量
        public String count(final Map<String, Object> params) {
            return new SQL(){
                {
                    SELECT("count(*)");
                    FROM(HrmConstants.DEPTTABLE);
                    if (params.get("dept") != null) {
                        Dept dept = (Dept) params.get("dept");
                        if (dept.getDepartName() != null && !"".equals(dept.getDepartName())) {
                            WHERE(" departname like concat ('%', #{dept.departName}, '%') ");
                        }   
                    }
                }
            }.toString();
        }
    
        // 动态插入
        public String insertDept(final Dept dept) {
            return new SQL(){
                {
                    INSERT_INTO(HrmConstants.DEPTTABLE);
                    if (dept.getDepartName() != null && !"".equals(dept.getDepartName())) {
                        VALUES("departname", "#{departName}");
                    }
                    if (dept.getRemark() != null && !"".equals(dept.getRemark())) {
                        VALUES("remark", "#{remark}");
                    }
                }
            }.toString();
        }
    
        // 动态更新
        public String updateDept(final Dept dept) {
            return new SQL(){
                {
                    UPDATE(HrmConstants.DEPTTABLE);
                    if (dept.getDepartName() != null) {
                        SET(" departname = #{departName}");
                    }
                    if (dept.getRemark() != null) {
                        SET(" remark = #{remark}");
                    }
                    WHERE(" id = #{id}");
                }
            }.toString();
        }
    }
    

    DeptService、DeptServiceImpl类:

    public interface DeptService {
        public List<Dept> findDeptList(Integer pageIndex, Integer pageSize);
    
        public Dept findDept(Integer id);
    
        public boolean addDept(Dept dept);
    
        public boolean removeDept(Integer id);
    
        public boolean modifyDept(Dept dept);
    }
    
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    @Service("deptService")
    public class DeptServiceImpl implements DeptService {
        @Autowired
        private DeptDao deptDao;
    
        @Override
        @Transactional(readOnly = true)
        public Dept findDept(Integer id) {
            return deptDao.selectById(id);
        }
    
        @Override
        public boolean addDept(Dept dept) {
            boolean isSuccess = false;
            try {
                // 这里row永远是返回受影响的行数
                int row = deptDao.save(dept);
                if (row > 0) {
                    isSuccess = true;
                }
            } catch (Exception e) {
                isSuccess = false;
            }
            return isSuccess;
        }
    
        @Override
        public boolean removeDept(Integer id) {
            boolean isSucces = false;
            try {
                Integer row = deptDao.deleteById(id);
                if (row > 0) {
                    isSucces = true;
                }
            } catch (Exception e) {
                isSucces = false;
            }
            return isSucces;
        }
    
        @Override
        public boolean modifyDept(Dept dept) {
            boolean isSuccess = false;
            try {
                int row = deptDao.update(dept);
                if (row > 0) {
                    isSuccess = true;
                }
            } catch (Exception e) {
                isSuccess = false;
            }
            return isSuccess;
        }
    
        @Override
        @Transactional(readOnly = true)
        public List<Dept> findDeptList(Integer pageIndex, Integer pageSize) {
            Map<String, Object> params = new HashMap<>();
            PageModel pageModel = new PageModel();
            pageModel.setPageIndex(pageIndex);
            if (pageSize <= 0) {
                pageSize = 10;
            }
            pageModel.setPageSize(pageSize);
            Integer count = deptDao.count(params);
            pageModel.setRecordCount(count);
            params.put("pageModel", pageModel);
            return deptDao.selectByPage(params);
        }
    }
    

    在这里需要特别注意的是,对数据进行插入、删除、修改的时候,要返回一个boolean变量值,告诉调用者有没有成功。对于mybatis来说,插入、删除、修改成功的时候,都会返回受影响的行数,也就是说大于0的,失败的时候会返回0。在数据插入的时候,有这样一条配置:

    @InsertProvider(type = DeptDynaSqlProvider.class, method = "insertDept")
    @SelectKey(statement = "SELECT LAST_INSERT_ID() AS id", keyProperty = "id", keyColumn = "id", before = false, resultType = Integer.class)
    Integer save(Dept dept);
    

    其中@SelectKey的作用就是把当前插入这条数据的主键id值,赋值给dept对象的id属性,这样就可以通过dept对象的id属性获取到mybatis数据库中的主键值,这对于有些接口业务实现是需要的。

    最后一步,我们来写单元测试来验证下,对dept表进行增删改查是否正常。

    查询部门列表:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class DeptServiceTests {
        @Autowired
        private DeptService deptService;
    
        @Test
        public void testDeptList() {
            List<Dept> deptList = deptService.findDeptList(1, 10);
            System.out.println("测试部门列表 : " + deptList);
            Assert.assertTrue(deptList.size() > 0);
        }
    }
    

    日志打印信息如下:

    spring-boot 1-2.png

    从日志信息可以看到SQL日志给打印出来了,另外,部门列表也给正确打印出来了。这说明数据库配置、mybatis配置都没有问题了,可以正常使用了。

    增加一个部门:

    @Test
    public void testAddDept() {
        Dept dept = new Dept("测试部1", "测试部1");
        boolean isSuccess = deptService.addDept(dept);
        System.out.println("增加部门的主键id:" + dept.getId());
        Assert.assertEquals(true, isSuccess);
    }
    
    @Test
    @Transactional
    public void testAddDept2() {
        Dept dept = new Dept("测试部2", "测试部2");
        boolean isSuccess = deptService.addDept(dept);
        System.out.println("增加部门的主键id: " + dept.getId());
        Assert.assertEquals(true, isSuccess);
    }
    

    这两个方法有什么区别呢?我们来跑下看下区别,运行testAddDept()方法得到的日志信息如下:

    spring-boot 1-3.png

    可以看到增加部门的主键id为46,查询数据库可以看到该条记录已经存在数据库当中了。如下图所示:

    spring-boot 1-4.png

    运行testAddDept2()得到的日志信息如下:

    spring-boot 1-5.png

    可以看到增加部门的主键id为47,但是查询数据库后却发现没有id为47这条数据,查询数据库结果如下所示:

    spring-boot 1-6.png

    这是为什么呢?这是因为@Transactional注解的原因,我们看到主键的id为47,这说明数据是插入成功了,否则的话id就不存在的,但是在这个测试方法执行完之后,事务进行了下回滚,所以数据库中就没有这条数据了。

    其他修改、删除测试方法,这里就不在叙述了,大家可以自己实现下。

    加密与解密

    Base64算法

    Base64算法最早应用于解决电子邮件传输的问题。Base64是一种基于64个字符的编码算法,根据RFC 2045的定义:Base64内容传送编码是一种以任意8位字节序列组合的描述形式,这种形式不易被人直接识别。经过Base64编码后的数据会比原始数据略长,为原来的4/3倍。经Base64编码后的字符串的字符数是以4为单位的整数倍。Base64算法在org.springframework.util包中有实现,下面来测试下base64的编码与解码,如下所示:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class EncryptTests {
        @Test
        public void testBase64() {
            String str = "123456";
            String encodeStr = Base64Utils.encodeToString(str.getBytes());
            System.out.println("base64 encode str : " + encodeStr);
            String decodeStr = new String(Base64Utils.decodeFromString(encodeStr));
            System.out.println("base64 decode str : " + decodeStr);
            Assert.assertEquals(str, decodeStr);
        }
    }
    

    如果str等于decodeStr则说明测试通过,日志信息如下:

    spring-boot 1-7.png

    MD5算法

    MD5算法主要用于验证数据的完整性,md5算法的实现分为两个部分,一部分是对字符串进行加密,一部分是对文件进行加密,实现如下所示:

    public class MD5Util {
        private static byte[] hexBase = {48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102};
    
        public static String fileHash(String filePath) {
            return fileHash(new File(filePath));
        }
    
        /**
        * 用DigestInputStream来计算大文件的md5,也避免内存吃不消。
        */
        public static String fileHash(File file) {
            if (file == null) {
                return "";
            }
    
            try {
                int bufferSize = 1024 * 1024;
                MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                FileInputStream fis = new FileInputStream(file);
                DigestInputStream dis = new DigestInputStream(fis, messageDigest);
                byte[] buffer = new byte[bufferSize];
                while (dis.read(buffer) > 0) ;
                messageDigest = dis.getMessageDigest();
                byte[] result = messageDigest.digest();
                return byteArrayToHex(result);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return "";
        }
    
        public static String stringMD5(String string) {
            return stringMD5(string, "utf-8");
        }
    
        public static String stringMD5(String string, String charsetName) {
            if (StringUtils.isEmpty(string)) {
                return "";
            }
            try {
                byte[] data = string.getBytes(charsetName);
                MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                messageDigest.update(data);
                byte[] result = messageDigest.digest();
                return byteArrayToHex(result);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            return "";
        }
    
        /**
        * md5对字符串加密
        * 方法一:使用hexBase 48, 49, 50, 51
        * 方法二:hexDigts 0 1 2 3 4 5
        * 这两种方法得到的md5加密结果是一样的。
        * 实现字节数组到十六进制的转换
        */
        public static String byteArrayToHex(byte[] bytes) {
            if (ArrayUtils.isEmpty(bytes)) {
                return "";
            }
    
            StringBuffer stringBuffer = new StringBuffer();
            int length = bytes.length;
            for (int i = 0; i < length; i++) {
                stringBuffer.append((char) hexBase[((bytes[i] & 0xF0) >> 4)]);
                stringBuffer.append((char) hexBase[(bytes[i] & 0xF)]);
            }
    
            return stringBuffer.toString();
        }
    
        public static String byteArrayToHex2(byte[] byteArray) {
            char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F' };
            char[] resultCharArray =new char[byteArray.length * 2];
            int index = 0;
            for (byte b : byteArray) {
                resultCharArray[index++] = hexDigits[b>>>4 & 0xf];
                resultCharArray[index++] = hexDigits[b & 0xf];
            }
            return new String(resultCharArray).toLowerCase();
        }
    
        public static String createMD5Sign(SortedMap signMap, String key) {
            StringBuffer stringBuffer = new StringBuffer();
            Set<Map.Entry<String, String>> paramSet = signMap.entrySet();
            for (Map.Entry<String, String> entry : paramSet) {
                String k = entry.getKey();
                if (k.equals("sign") || k.equals("mysign") || k.equals("code")) {
                    continue;
                }
                String v = entry.getValue();
                stringBuffer.append(k + "=" + v + "&");
            }
            String params = stringBuffer.append("key=" + key).toString();
            return stringMD5(params, "utf-8").toUpperCase();
        }
    }
    

    先对字符串md5加密进行下测试,如下:

    @Test
    public void testMD5Str() {
        String str = "456789123456";
        String md5Str = MD5Util.stringMD5(str);
        System.out.println("md5 str : " + md5Str);
        Assert.assertTrue(md5Str.length() == 32);
    }
    

    解释下为什么md5Str的长度等于32就认为md5加密成功了,因为md5算法需要获得一个随机长度的信息并产生一个128位的信息摘要,如果将这个128位的二进制摘要信息换算成十六进制,可以得到一个32位的字符串。日志信息如下:

    spring-boot 1-8.png

    再测试一下对文件进行md5加密,文件为电脑桌面的一张图片,如下:

    @Test
    public void testMD5File() {
        File file = new File("/Users/Bruce/Desktop/hrm4.jpg");
        String md5File = MD5Util.fileHash(file);
        System.out.println("md5 file : " + md5File);
        Assert.assertTrue(md5File.length() == 32);
    }
    

    日志信息如下:

    spring-boot 1-9.png

    在对大文件进行加密的时候,一定要使用DigestInputStream,这样可以避免内存吃不消,也可以使加密速度大大提高。

    AES算法

    AES算法的出现主要就是为了解决DES算法出现的漏洞,实现如下:

    public class AESUtil {
        public static String encrypt(String data, String key, String iv) {
            return encrypt(data, key, iv, "utf-8");
        }
    
        public static String encrypt(String data, String key, String iv, String charsetName) {
            try {
                return encrypt(data.getBytes(charsetName), key.getBytes(charsetName), iv.getBytes(charsetName));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return "";
        }
    
        public static String encrypt(byte[] data, byte[] key, byte[] iv) {
            // 这里的key为了与iOS统一,不可以使用KeyGenerator、SecureRandom、SecretKey生成
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            try {
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
                byte[] result = cipher.doFinal(data);
                // 对加密后的数据,尽心base64编码
                return Base64Util.encode(result);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidAlgorithmParameterException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            }
            return "";
        }
    
        public static String decrypt(String data, String key, String iv){
            return decrypt(data, key, iv, "utf-8");
        }
    
        public static String decrypt(String data, String key, String iv, String charsetName) {
            try {
                byte[] contentData = Base64Util.decodeBytes(data.getBytes(charsetName));
                return decrypt(contentData, key.getBytes(charsetName), iv.getBytes(charsetName), charsetName);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return "";
        }
    
        public static String decrypt(byte[] data, byte[] key, byte[] iv, String charsetName) {
            SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            try {
                Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
                byte[] result = cipher.doFinal(data);
                return new String(result, charsetName);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidAlgorithmParameterException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
    

    对AES算法进行下测试,如下:

    @Test
    public void testAES() {
        String str = "0987654f321";
        String encryptStr = AESUtil.encrypt(str, BootConstants.AES_KEY, BootConstants.AES_IV);
        System.out.println("encryptStr : " + encryptStr);
        String decryptStr = AESUtil.decrypt(encryptStr, BootConstants.AES_KEY, BootConstants.AES_IV);
        System.out.println("decryptStr : " + decryptStr);
        Assert.assertEquals(str, decryptStr);
    }
    

    其中KEY、IV都是我们自定义的,具体可以看下Java加密与解密这本书,上面对KEY和IV有详细的解释,日志信息如下所示:

    spring-boot 1-10.png

    接口

    第一步,根据Restful接口设计,需要实现的接口列表如下所示:

    GET /hrm/api/depts 查询部门列表
    GET /hrm/api/depts/id 查询指定部门信息
    POST /hrm/api/depts 增加一个部门
    DELETE /hrm/api/depts/id 删除指定部门
    PUT /hrm/api/depts/id 更新某个部门信息(需要提供整个部门的信息)
    PATCH /hrm/api/depts/id 更新某个部门信息(需要提供部门的部分信息)
    

    第二步,自定义注解,因为请求接口,有些参数是必须要传的,这个时候就要用到自定义注解了,如下所示:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataValidate {
        String[] requiredParams() default {};
    }
    

    第三步:配置拦截器,每一个请求过来,都要先进行下验证,验证sign签名是否正确,验证请求时间是否有效,验证必传参数是否传过来了,这些都需要用到拦截器的,比如验证sign签名是否正确:

        // 对数据签名进行校验 需要的是sortedMap,看看这样转换是不是可以的
        String sign = jsonObject.getString("sign");
        Map<String, Object> map = JSONObject.toJavaObject(jsonObject, Map.class);
        // TreeMap默认是升序排列的,如果要改为降序采用其他方式来实现
        SortedMap<String, Object> sortedMap = new TreeMap<>(map);
        String sysSign = MD5Util.createMD5Sign(sortedMap, BootConstants.SIGN_KEY);
        if (!sysSign.equals(sign)) {
            logger.info("数据签名校验失败");
            // 在这里返回错误的信息给客户端
            JSONObject responseJson = new JSONObject();
            responseJson.put(BootConstants.CODE_KEY, StatusCode.ERROR_SIGN_INVALIDATE);
            responseJson.put(BootConstants.MESSAGE_KEY, "数据签名校验失败");
            String jsonStr = JSON.toJSONString(responseJson, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty);
            String responseStr = AESUtil.encrypt(jsonStr, BootConstants.AES_KEY, BootConstants.AES_IV);
            response.getWriter().write(responseStr);
            return false;
        }
    

    完整的拦截器代码,会在文章末尾开源出来自己写的Java后台小项目

    第三步,定义表的常量、项目当中要用到的一些常量,如下所示:

    public class StatusCode {
        public static final Integer SUCCESS = 0; // 成功
        public static final Integer ERROR = -1; //错误信息
    
        // app端使用100开头的
        public static final Integer ERROR_TIMEOUT = 100; // 请求时间不合法
        public static final Integer ERROR_SIGN_INVALIDATE = 101; // 签名校验失败
        public static final Integer ERROR_REQUIREDPARAMS_LOST = 102; // 必传参数未传
        public static final Integer ERROR_DATA_EMPTY = 103; // 数据为空
        public static final Integer ERROR_UNKNOW = 104; // 系统错误
    
        // 后台网站使用200开头的 以后其他类型以此类推 通用的错误信息用状态码给表示出来,特殊的错误信息用code = -1来标识即可
    }
    
    public class HrmConstants {
        // 数据库常量表
        public static final String USERTABLE = "user_inf";
        public static final String DEPTTABLE = "dept_inf";
        public static final String JOBTABLE = "job_inf";
        public static final String EMPLOYEETABLE = "employee_inf";
        public static final String NOTICETABLE = "notice_inf";
        public static final String DOCUMENTTABLE = "document_inf";
    
        // 登录
        public static final String LOGIN = "loginForm";
        // 用户的Session对象
        public static final String USER_SESSION = "user_session";
        // 默认每页4条数据
        public static int PAGE_DEFAULT_SIZE = 4;
    }
    

    现在我们来写个单元测试,来验证下配置的拦截器是否起作用,测试时间是否有效为例,剩下的可以在开源项目中找到:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class DataSecurityInterceptorTests {
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        public void testTimeValidate() {
            SortedMap<String, String> sortedMap = new TreeMap<>();
            //sortedMap.put("timeStamp", System.currentTimeMillis()+"");
            sortedMap.put("timeStamp", "1509416666000");
            sortedMap.put("deptId", "1");
            StringBuilder stringBuilder = new StringBuilder();
            for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
                stringBuilder.append(entry.getKey() + "=" + entry.getValue());
            }
            String sign = MD5Util.createMD5Sign(sortedMap, BootConstants.SIGN_KEY);
            System.out.println("sign : " + sign);
            sortedMap.put("sign", sign);
    
            String mapStr = JSON.toJSONString(sortedMap, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty);
            String encryptStr = AESUtil.encrypt(mapStr, BootConstants.AES_KEY, BootConstants.AES_IV);
    
            try {
                MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/hrm/api/depts/id")
                    .param("Encrypt", encryptStr)
                    .accept(MediaType.APPLICATION_JSON))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print())
                    .andReturn();
                String content = mvcResult.getResponse().getContentAsString();
                System.out.println("content = " + content);
                String decryptStr = AESUtil.decrypt(content, BootConstants.AES_KEY, BootConstants.AES_IV);
                System.out.println("解密后的字符串: " + decryptStr);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    第四步,编写DeptController的代码,以获取部门列表,其余完整的,可以在文章末尾开源的项目中找到:

    @RestController
    public class DeptController {
        private static final Logger logger = LoggerFactory.getLogger(DeptController.class);
        @Autowired
        @Qualifier("deptService")
        private DeptService deptService;
    
        /**
        * 在查询列表的时候,有这样一个情况:如果当前页面是否超过了总页数:如果超过了默认给最后一页作为当前页。
        */
        @RequestMapping(value = "/hrm/api/depts", method = RequestMethod.GET)
        @DataValidate(requiredParams = {"pageIndex"})
        public void selectDeptList(HttpServletRequest request, HttpServletResponse response) {
            JSONObject requestJson = (JSONObject) request.getAttribute(BootConstants.REQUESTDATA);
            if (!requestJson.isEmpty()) {
                JSONObject resultJson = new JSONObject();
    
                String pageIndex = requestJson.getString("pageIndex");
                String pageSize = requestJson.getString("pageSize");
                if (StringUtils.isEmpty(pageSize)) {
                    pageSize = "10";
                }
                List<Dept> deptList = deptService.findDeptList(Integer.parseInt(pageIndex), Integer.parseInt(pageSize));
                resultJson.put(BootConstants.CODE_KEY, StatusCode.SUCCESS);
                resultJson.put(BootConstants.MESSAGE_KEY, "请求成功");
                if (deptList != null && deptList.size() > 0) {
                    String deptStr = JSON.toJSONString(deptList, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty);
                    JSONArray deptJsonArray = JSONArray.parseArray(deptStr);
                    resultJson.put(BootConstants.DATA_KEY, deptJsonArray);
                } else {
                    JSONArray emptyJsonArray = new JSONArray();
                    resultJson.put(BootConstants.DATA_KEY, emptyJsonArray);
                }
    
                request.setAttribute(BootConstants.REQUESTAFTERDATA, resultJson);
            }
        }
    }
    

    在编写Controller代码的时候,特别要注意fastjson的问题,fastjson默认对列表会存在循环引用,也就是会出现ref的情况,这个时候就要把循环引用给关闭掉。另外,fastjson默认也会把属性值为null或者空的给过滤掉,不显示出来,要设置其序列化的属性才可以让其给显示出来。这点觉得fastjson设计的不太友好,可能阿里巴巴内部因为业务需求是这样要求的,对阿里巴巴内部使用可能比较方便,但是开源出来,面向大众的组件,就不应该这样设计了,吐槽一下。

    第五步,我们通过两种测试来验证下请求部门列表是否正常,第一种使用mockMvc来进行测试,第二种使用httpclient来发送真实的网络请求来测试,首先看下第一种测试,如下所示:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class DeptControllerTests {
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        private DeptService deptService;
    
        @Before
        public void setUp() throws Exception {
            Dept dept = new Dept();
            dept.setId(1);
            dept.setDepartName("研发部");
            dept.setRemark("研发部");
            // given的主要作用就是对controller下面的接口进行下快速验证而已,并不会发所有的数据全部返回给你的。
            BDDMockito.given(deptService.findDeptList(1, 10)).willReturn(Arrays.asList(dept));
        }
    
        @Test
        public void testDeptList() throws Exception {
            SortedMap<String, String> sortedMap = new TreeMap<>();
            sortedMap.put("timeStamp", System.currentTimeMillis()+"");
            sortedMap.put("pageIndex", "1");
            StringBuilder stringBuilder = new StringBuilder();
            for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
                stringBuilder.append(entry.getKey() + "=" + entry.getValue());
            }
            String sign = MD5Util.createMD5Sign(sortedMap, BootConstants.SIGN_KEY);
            System.out.println("sign : " + sign);
            sortedMap.put("sign", sign);
    
            String mapStr = JSON.toJSONString(sortedMap, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty);
            String encryptStr = AESUtil.encrypt(mapStr, BootConstants.AES_KEY, BootConstants.AES_IV);
    
            MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/hrm/api/depts")
                .param("Encrypt", encryptStr)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
            String content = mvcResult.getResponse().getContentAsString();
            System.out.println("content = " + content);
            String decryptStr = AESUtil.decrypt(content, BootConstants.AES_KEY, BootConstants.AES_IV);
            JSONObject resultJson = JSONObject.parseObject(decryptStr);
            System.out.println("部门列表测试信息 : " + resultJson);
        }
    }
    

    日志信息如下所示:

    spring-boot 1-11.png

    使用httpClient来进行下测试,如下所示:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class DeptHttpClientTests {
        /**
        * 测试GET方法:部门列表、单独一个部门
        */
        @Test
        public void testDeptList() {
            SortedMap<String, String> sortedMap = new TreeMap<>();
            sortedMap.put("pageIndex", "1");
            String encryptStr = EncryptUtils.getEncryptStr(sortedMap);
            try {
                URIBuilder builder = new URIBuilder(BootConstants.LOCAL_HOST+"/hrm/api/depts")
                    .addParameter("Encrypt", encryptStr);
                String content = HttpUtils.sendGetRequest(builder.build());
                System.out.println("content = " + content);
                String decryptStr = EncryptUtils.getDecryptStr(content);
                JSONObject resultJson = JSONObject.parseObject(decryptStr);
                System.out.println("部门列表测试信息 : " + resultJson);
            } catch (URISyntaxException e) {
                e.printStackTrace();
            }
        }
    }
    

    日志信息如下:

    spring-boot 1-12.png

    从日志信息可以看出两者的区别了,使用mockMvc的方式只是把假定的列表值给打印出来了,这种方式也验证了其查询部门列表方法是成功的。使用httpclient发送真实的网络请求把所有的部门列表值都给打印出来了。两种方式都有具体的使用场景,读者可以根据自己的需要去使用。

    总结

    本篇文章大概介绍了下写一个完整接口的所有步骤和流程,具体的代码实现可以参考我开源出来的项目代码。

    Java后台小项目开源地址

    相关文章

      网友评论

        本文标题:如何使用Spring Boot从0到1搭建一个Java后台(二)

        本文链接:https://www.haomeiwen.com/subject/xkodmxtx.html