Spring Cloud iToken实战项目(十):创建文章服务提供者(整合Mybatis 二级缓存 和 Swagger2接口文档引擎)

1 创建文章服务提供者

1.1 创建项目

创建一个名为itoken-service-posts 的服务提供者项目

1.2 服务所需数据库脚本

/*
SQLyog  v12.2.6 (64 bit)
MySQL - 5.7.22 : Database - itoken-service-admin
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`itoken-service-posts` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

USE `itoken-service-posts`;

DROP TABLE IF EXISTS tb_posts_post;

-- 文章表
CREATE TABLE tb_posts_post
(
	post_guid varchar(100) NOT NULL COMMENT '文章编码',
    title varchar(100) NOT NULL COMMENT '文章标题',
    time_published datetime NOT NULL COMMENT '文章发布时间',
    status char NOT NULL COMMENT '文章状态(0草稿 1已发布的文章 2待审核的文章 3被拒绝文章 4定时发布的文章)',
    alias varchar(100) COMMENT '文章别名',
    score decimal(3) DEFAULT 0 COMMENT '文章得分',
    summary text COMMENT '文章摘要',
    main text COMMENT '文章正文',
    authors text COMMENT '文章作者对象',
    thumb_image text COMMENT '封面缩略图片',
    original_images text COMMENT '裁剪后不带尺寸的正文图片数组',
    images text COMMENT '裁剪后带尺寸的正文图片数组',
    full_size_images text COMMENT '裁剪前的正文图片数组',
    tags text COMMENT '文章标签',
    v_tags text COMMENT '文章特色标签',
    number_of_upvotes decimal(9) DEFAULT 0 COMMENT '被赞数',
    number_of_downvotes decimal(9) DEFAULT 0 COMMENT '被踩数',
    number_of_reads decimal(9) DEFAULT 0 COMMENT '被阅读数',
    number_of_shares decimal(9) DEFAULT 0 COMMENT '被分享数',
    number_of_bookmarks decimal(9) DEFAULT 0 COMMENT '被收藏数',
    number_of_comments decimal(9) DEFAULT 0 COMMENT '被评论数',
    reject_msg varchar(100) COMMENT '文章审核被拒理由',
    series text COMMENT '一篇文章的系列集合',
    access char(2) COMMENT '文章的阅读权限(0无限制 1会员)',
	create_by varchar(64) NOT NULL COMMENT '创建者',
	create_date datetime NOT NULL COMMENT '创建时间',
	update_by varchar(64) NOT NULL COMMENT '更新者',
	update_date datetime NOT NULL COMMENT '更新时间',
	remarks varchar(500) COMMENT '备注信息',
	extend_s1 varchar(500) COMMENT '扩展 String 1',
	extend_s2 varchar(500) COMMENT '扩展 String 2',
	extend_s3 varchar(500) COMMENT '扩展 String 3',
	extend_s4 varchar(500) COMMENT '扩展 String 4',
	extend_s5 varchar(500) COMMENT '扩展 String 5',
	extend_s6 varchar(500) COMMENT '扩展 String 6',
	extend_s7 varchar(500) COMMENT '扩展 String 7',
	extend_s8 varchar(500) COMMENT '扩展 String 8',
	extend_i1 decimal(19) COMMENT '扩展 Integer 1',
	extend_i2 decimal(19) COMMENT '扩展 Integer 2',
	extend_i3 decimal(19) COMMENT '扩展 Integer 3',
	extend_i4 decimal(19) COMMENT '扩展 Integer 4',
	extend_f1 decimal(19,4) COMMENT '扩展 Float 1',
	extend_f2 decimal(19,4) COMMENT '扩展 Float 2',
	extend_f3 decimal(19,4) COMMENT '扩展 Float 3',
	extend_f4 decimal(19,4) COMMENT '扩展 Float 4',
	extend_d1 datetime COMMENT '扩展 Date 1',
	extend_d2 datetime COMMENT '扩展 Date 2',
	extend_d3 datetime COMMENT '扩展 Date 3',
	extend_d4 datetime COMMENT '扩展 Date 4',
	PRIMARY KEY (post_guid)
) COMMENT = '文章表';

CREATE INDEX idx_posts_post_pg ON tb_posts_post (post_guid ASC);

2 配置 MyBatis Redis 二级缓存

2.1 概述

注:由于之前的文章已经配置过 Redis 这里不再赘述

2.2 一级缓存

MyBatis 会在表示会话的 SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

一级缓存是SqlSession 级别的缓存。在操作数据库时需要构造sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的。其作用域是同一个 SqlSession,在同一个 sqlSession 中两次执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个 sqlSession 结束后该 sqlSession 中的一级缓存也就不存在了。Mybatis 默认开启一级缓存。

2.3 二级缓存

二级缓存是 mapper 级别的缓存,多个SqlSession去操作同一个 Mapper 的 sql 语句,多个 SqlSession 去操作数据库得到数据会存在二级缓存区域,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。其作用域是 mapper 的同一个 namespace,不同的 sqlSession 两次执行相同namespace下的 sql 语句且向 sql 中传递参数也相同即最终执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis 默认没有开启二级缓存需要在 setting 全局参数中配置开启二级缓存。

2.4 开启 MyBatis 二级缓存

在 Spring Boot 配置文件中开启 MyBatis 二级缓存,配置代码如下:

mybatis:
  configuration:
    cache-enabled: true

2.5 实体类实现序列化接口并声明序列号

private static final long serialVersionUID = 8289770415244673535L;

2.6 IDEA 提示生成序列号

默认情况下 Intellij IDEA 不会提示继承了 Serializable 接口的类生成 serialVersionUID 的警告。如果需要生成 serialVersionUID,需要手动配置。

File -> Settings -> Inspections -> Serialization issues -> Serialization class without ‘serialVersionUID’

在这里插入图片描述

2.7 创建相关工具类

2.7.1 实现 Spring ApplicationContextAware 接口,用于手动注入 Bean

创建一个名为ApplicationContextHolder的工具类,代码如下:

package com.funtl.itoken.common.context;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {
    private static final Logger logger = LoggerFactory.getLogger(ApplicationContextHolder.class);

    private static ApplicationContext applicationContext;

    /**
     * 获取存储在静态变量中的 ApplicationContext
     *
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        assertContextInjected();
        return applicationContext;
    }

    /**
     * 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
     *
     * @param name
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name) {
        assertContextInjected();
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        assertContextInjected();
        return applicationContext.getBean(clazz);
    }

    /**
     * 实现 DisposableBean 接口,在 Context 关闭时清理静态变量
     *
     * @throws Exception
     */
    public void destroy() throws Exception {
        logger.debug("清除 SpringContext 中的 ApplicationContext: {}", applicationContext);
        applicationContext = null;
    }

    /**
     * 实现 ApplicationContextAware 接口,注入 Context 到静态变量中
     *
     * @param applicationContext
     * @throws BeansException
     */
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.applicationContext = applicationContext;
    }

    /**
     * 断言 Context 已经注入
     */
    private static void assertContextInjected() {
        Validate.validState(applicationContext != null, "applicationContext 属性未注入,请在 spring-context.xml 配置中定义 ApplicationContextHolder");
    }
}

2.7.2 实现 MyBatis Cache 接口,用于自定义缓存为 Redis

创建一个名为RedisCache 的工具类,代码如下:

package com.funtl.itoken.common.utils;

import com.funtl.itoken.common.context.ApplicationContextHolder;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Redis 缓存工具类
 * <p>Title: RedisCache</p>
 * <p>Description: </p>
 *
 * @author Lusifer
 * @version 1.0.0
 * @date 2018/8/13 6:03
 */
public class RedisCache implements Cache {
    private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final String id; // cache instance id
    private RedisTemplate redisTemplate;

    private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    /**
     * Put query result to redis
     *
     * @param key
     * @param value
     */
    @Override
    public void putObject(Object key, Object value) {
        try {
            RedisTemplate redisTemplate = getRedisTemplate();
            ValueOperations opsForValue = redisTemplate.opsForValue();
            opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
            logger.debug("Put query result to redis");
        } catch (Throwable t) {
            logger.error("Redis put failed", t);
        }
    }

    /**
     * Get cached query result from redis
     *
     * @param key
     * @return
     */
    @Override
    public Object getObject(Object key) {
        try {
            RedisTemplate redisTemplate = getRedisTemplate();
            ValueOperations opsForValue = redisTemplate.opsForValue();
            logger.debug("Get cached query result from redis");
//            System.out.println("****" + opsForValue.get(key).toString());
            return opsForValue.get(key);
        } catch (Throwable t) {
            logger.error("Redis get failed, fail over to db", t);
            return null;
        }
    }

    /**
     * Remove cached query result from redis
     *
     * @param key
     * @return
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object removeObject(Object key) {
        try {
            RedisTemplate redisTemplate = getRedisTemplate();
            redisTemplate.delete(key);
            logger.debug("Remove cached query result from redis");
        } catch (Throwable t) {
            logger.error("Redis remove failed", t);
        }
        return null;
    }

    /**
     * Clears this cache instance
     */
    @Override
    public void clear() {
        RedisTemplate redisTemplate = getRedisTemplate();
        redisTemplate.execute((RedisCallback) connection -> {
            connection.flushDb();
            return null;
        });
        logger.debug("Clear all the cached query result from redis");
    }

    /**
     * This method is not used
     *
     * @return
     */
    @Override
    public int getSize() {
        return 0;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    private RedisTemplate getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
        }
        return redisTemplate;
    }
}

2.8 Mapper 接口中增加注解

在 Mapper 接口中增加注解,声明需要使用二级缓存

package com.funtl.itoken.common.mapper;

import com.funtl.itoken.common.domain.TbSysUser;
import com.funtl.itoken.common.utils.RedisCache;
import org.apache.ibatis.annotations.CacheNamespace;
import tk.mybatis.mapper.MyMapper;

@CacheNamespace(implementation = RedisCache.class)
public interface TbSysUserMapper extends MyMapper<TbSysUser> {
}

3 配置 Swagger2 接口文档引擎

3.1 手写文档存在的问题

  • 文档需要更新的时候,需要再次发送一份给前端,也就是文档更新交流不及时。
  • 接口返回结果不明确
  • 不能直接在线测试接口,通常需要使用工具,比如:Postman
  • 接口文档太多,不好管理

3.2 使用 Swagger 解决问题

Swagger 也就是为了解决这个问题,当然也不能说 Swagger 就一定是完美的,当然也有缺点,最明显的就是代码植入性比较强。

3.2.1 POM依赖

增加 Swagger2 所需依赖,pom.xml 配置如下:

<!-- Swagger2 Begin -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.8.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.8.0</version>
</dependency>
<!-- Swagger2 End -->

3.2.2 配置 Swagger2

注意:RequestHandlerSelectors.basePackage(“com.funtl.itoken.service.admin.controller”) 为 Controller 包路径,不然生成的文档扫描不到接口,每个项目单独配置

创建一个名为 Swagger2Config 的 Java 配置类,代码如下:

package com.funtl.itoken.service.admin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.funtl.itoken.service.admin.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("iToken API 文档")
                .description("iToken API 网关接口,http://www.funtl.com")
                .termsOfServiceUrl("http://www.funtl.com")
                .version("1.0.0")
                .build();
    }
}

3.2.3 启用 Swagger2

Application 中加上注解 @EnableSwagger2 表示开启 Swagger

package com.funtl.itoken.service.admin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication(scanBasePackages = "com.funtl.itoken")
@EnableEurekaClient
@EnableSwagger2
@MapperScan(basePackages = {"com.funtl.itoken.common.mapper", "com.funtl.itoken.service.admin.mapper"})
public class ServiceAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceAdminApplication.class, args);
    }
}

3.2.4 使用 Swagger2

在 Controller 中增加 Swagger2 相关注解,代码如下:

/**
 * 分页查询
 *
 * @param pageNum
 * @param pageSize
 * @param tbSysUserJson
 * @return
 */
@ApiOperation(value = "管理员分页查询")
@ApiImplicitParams({
        @ApiImplicitParam(name = "pageNum", value = "页码", required = true, dataType = "int", paramType = "path"),
        @ApiImplicitParam(name = "pageSize", value = "笔数", required = true, dataType = "int", paramType = "path"),
        @ApiImplicitParam(name = "tbSysUserJson", value = "管理员对象 JSON 字符串", required = false, dataTypeClass = String.class, paramType = "json")
})
@RequestMapping(value = "page/{pageNum}/{pageSize}", method = RequestMethod.GET)
public BaseResult page(
        @PathVariable(required = true) int pageNum,
        @PathVariable(required = true) int pageSize,
        @RequestParam(required = false) String tbSysUserJson
) throws Exception {

    TbSysUser tbSysUser = null;
    if (tbSysUserJson != null) {
        tbSysUser = MapperUtils.json2pojo(tbSysUserJson, TbSysUser.class);
    }
    PageInfo pageInfo = adminService.page(pageNum, pageSize, tbSysUser);

    // 分页后的结果集
    List<TbSysUser> list = pageInfo.getList();

    // 封装 Cursor 对象
    BaseResult.Cursor cursor = new BaseResult.Cursor();
    cursor.setTotal(new Long(pageInfo.getTotal()).intValue());
    cursor.setOffset(pageInfo.getPageNum());
    cursor.setLimit(pageInfo.getPageSize());

    return BaseResult.ok(list, cursor);
}

3.2.5 Swagger 注解说明

Swagger 通过注解表明该接口会生成文档,包括接口名、请求方法、参数、返回信息的等等。

注释说明
@Api修饰整个类,描述 Controller 的作用
@ApiOperation描述一个类的一个方法,或者说一个接口
@ApiParam单个参数描述
@ApiModel用对象来接收参数
@ApiProperty用对象接收参数时,描述对象的一个字段
@ApiResponseHTTP 响应其中 1 个描述
@ApiResponsesHTTP 响应整体描述
@ApiIgnore使用该注解忽略这个API
@ApiError发生错误返回的信息
@ApiImplicitParam一个请求参数
@ApiImplicitParams多个请求参数

3.2.6 访问 Swagger2

访问地址:http://服务器地址/swagger-ui.html

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值