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 | 用对象接收参数时,描述对象的一个字段 |
@ApiResponse | HTTP 响应其中 1 个描述 |
@ApiResponses | HTTP 响应整体描述 |
@ApiIgnore | 使用该注解忽略这个API |
@ApiError | 发生错误返回的信息 |
@ApiImplicitParam | 一个请求参数 |
@ApiImplicitParams | 多个请求参数 |