metadata = server.getInstanceInfo().getMetadata();
if (StrUtil.isBlank(metadata.get(SecurityConstants.VERSION))) {
log.debug("当前微服务{} 未配置版本直接路由");
return true;
}
if (metadata.get(SecurityConstants.VERSION).equals(targetVersion)) {
return true;
} else {
log.debug("当前微服务{} 版本为{},目标版本{} 匹配失败", server.getInstanceInfo().getAppName()
, metadata.get(SecurityConstants.VERSION), targetVersion);
return false;
}
}
};
}
```
动态路由
----
### 配置
```
public class DynamicRouteLocator extends DiscoveryClientRouteLocator {
private ZuulProperties properties;
private RedisTemplate redisTemplate;
public DynamicRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties,
ServiceInstance localServiceInstance, RedisTemplate redisTemplate) {
super(servletPath, discovery, properties, localServiceInstance);
this.properties = properties;
this.redisTemplate = redisTemplate;
}
/**
* 重写路由配置
*
* 1. properties 配置。
* 2. eureka 默认配置。
* 3. DB数据库配置。
*
* @return 路由表
*/
@Override
protected LinkedHashMap locateRoutes() {
LinkedHashMap routesMap = new LinkedHashMap<>();
//读取properties配置、eureka默认配置
routesMap.putAll(super.locateRoutes());
log.debug("初始默认的路由配置完成");
routesMap.putAll(locateRoutesFromDb());
LinkedHashMap values = new LinkedHashMap<>();
for (Map.Entry entry : routesMap.entrySet()) {
String path = entry.getKey();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StrUtil.isNotBlank(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
/**
* Redis中保存的,没有从upms拉去,避免启动链路依赖问题(取舍),网关依赖业务模块的问题
*
* @return
*/
private Map locateRoutesFromDb() {
Map routes = new LinkedHashMap<>();
Object obj = redisTemplate.opsForValue().get(CommonConstant.ROUTE_KEY);
if (obj == null) {
return routes;
}
List results = (List) obj;
for (SysZuulRoute result : results) {
if (StrUtil.isBlank(result.getPath()) && StrUtil.isBlank(result.getUrl())) {
continue;
}
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
try {
zuulRoute.setId(result.getServiceId());
zuulRoute.setPath(result.getPath());
zuulRoute.setServiceId(result.getServiceId());
zuulRoute.setRetryable(StrUtil.equals(result.getRetryable(), "0") ? Boolean.FALSE : Boolean.TRUE);
zuulRoute.setStripPrefix(StrUtil.equals(result.getStripPrefix(), "0") ? Boolean.FALSE : Boolean.TRUE);
zuulRoute.setUrl(result.getUrl());
List sensitiveHeadersList = StrUtil.splitTrim(result.getSensitiveheadersList(), ",");
if (sensitiveHeadersList != null) {
Set sensitiveHeaderSet = CollUtil.newHashSet();
sensitiveHeadersList.forEach(sensitiveHeader -> sensitiveHeaderSet.add(sensitiveHeader));
zuulRoute.setSensitiveHeaders(sensitiveHeaderSet);
zuulRoute.setCustomSensitiveHeaders(true);
}
} catch (Exception e) {
log.error("从数据库加载路由配置异常", e);
}
log.debug("添加数据库自定义的路由配置,path:{},serviceId:{}", zuulRoute.getPath(), zuulRoute.getServiceId());
routes.put(zuulRoute.getPath(), zuulRoute);
}
return routes;
}
}
```
网关日志处理
------
代码注释已经将逻辑写的很清楚了
```
@Slf4j
@Component
public class LogSendServiceImpl implements LogSendService {
private static final String SERVICE_ID = "serviceId";
@Autowired
private AmqpTemplate rabbitTemplate;
/**
* 1. 获取 requestContext 中的请求信息
* 2. 如果返回状态不是OK,则获取返回信息中的错误信息
* 3. 发送到MQ
*
* @param requestContext 上下文对象
*/
@Override
public void send(RequestContext requestContext) {
HttpServletRequest request = requestContext.getRequest();
String requestUri = request.getRequestURI();
String method = request.getMethod();
SysLog sysLog = new SysLog();
sysLog.setType(CommonConstant.STATUS_NORMAL);
sysLog.setRemoteAddr(HttpUtil.getClientIP(request));
sysLog.setRequestUri(URLUtil.getPath(requestUri));
sysLog.setMethod(method);
sysLog.setUserAgent(request.getHeader("user-agent"));
sysLog.setParams(HttpUtil.toParams(request.getParameterMap()));
Long startTime = (Long) requestContext.get("startTime");
sysLog.setTime(System.currentTimeMillis() - startTime);
if (requestContext.get(SERVICE_ID) != null) {
sysLog.setServiceId(requestContext.get(SERVICE_ID).toString());
}
//正常发送服务异常解析
if (requestContext.getResponseStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR
&& requestContext.getResponseDataStream() != null) {
InputStream inputStream = requestContext.getResponseDataStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream stream1 = null;
InputStream stream2;
byte[] buffer = IoUtil.readBytes(inputStream);
try {
baos.write(buffer);
baos.flush();
stream1 = new ByteArrayInputStream(baos.toByteArray());
stream2 = new ByteArrayInputStream(baos.toByteArray());
String resp = IoUtil.read(stream1, CommonConstant.UTF8);
sysLog.setType(CommonConstant.STATUS_LOCK);
sysLog.setException(resp);
requestContext.setResponseDataStream(stream2);
} catch (IOException e) {
log.error("响应流解析异常:", e);
throw new RuntimeException(e);
} finally {
IoUtil.close(stream1);
IoUtil.close(baos);
IoUtil.close(inputStream);
}
}
//网关内部异常
Throwable throwable = requestContext.getThrowable();
if (throwable != null) {
log.error("网关异常", throwable);
sysLog.setException(throwable.getMessage());
}
//保存发往MQ(只保存授权)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && StrUtil.isNotBlank(authentication.getName())) {
LogVO logVo = new LogVO();
sysLog.setCreateBy(authentication.getName());
logVo.setSysLog(sysLog);
logVo.setUsername(authentication.getName());
rabbitTemplate.convertAndSend(MqQueueConstant.LOG_QUEUE, logVo);
}
}
}
```
多维度限流
-----
### 限流降级处理器 ZuulRateLimiterErrorHandler
重写 zuul 中默认的限流处理器`DefaultRateLimiterErrorHandler`,使之记录日志内容
```
@Bean
public RateLimiterErrorHandler rateLimitErrorHandler() {
return new DefaultRateLimiterErrorHandler() {
@Override
public void handleSaveError(String key, Exception e) {
log.error("保存key:[{}]异常", key, e);
}
@Override
public void handleFetchError(String key, Exception e) {
log.error("路由失败:[{}]异常", key);
}
@Override
public void handleError(String msg, Exception e) {
log.error("限流异常:[{}]", msg, e);
}
};
}
```
与 spring security oAuth 方法整合单点登陆
--------------------------------
### 授权拒绝处理器 PigAccessDeniedHandler
重写`Srping security oAuth` 提供单点登录验证拒绝`OAuth2AccessDeniedHandler`接口,使用 R 包装失败信息到`PigDeniedException`
```
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {
log.info("授权失败,禁止访问 {}", request.getRequestURI());
response.setCharacterEncoding(CommonConstant.UTF8);
response.setContentType(CommonConstant.CONTENT_TYPE);
R result = new R<>(new PigDeniedException("授权失败,禁止访问"));
response.setStatus(HttpStatus.SC_FORBIDDEN);
PrintWriter printWriter = response.getWriter();
printWriter.append(objectMapper.writeValueAsString(result));
}
```
菜单管理
----
### MenuService
```
@FeignClient(name = "pig-upms-service", fallback = MenuServiceFallbackImpl.class)
public interface MenuService {
/**
* 通过角色名查询菜单
*
* @param role 角色名称
* @return 菜单列表
*/
@GetMapping(value = "/menu/findMenuByRole/{role}")
Set findMenuByRole(@PathVariable("role") String role);
}
```
使用 feign 连接 pig 系统的菜单微服务
#### 菜单权限
```
@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {
@Autowired
private MenuService menuService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//ele-admin options 跨域配置,现在处理是通过前端配置代理,不使用这种方式,存在风险
// if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
// return true;
// }
Object principal = authentication.getPrincipal();
List authorityList = (List) authentication.getAuthorities();
AtomicBoolean hasPermission = new AtomicBoolean(false);
if (principal != null) {
if (CollUtil.isEmpty(authorityList)) {
log.warn("角色列表为空:{}", authentication.getPrincipal());
return false;
}
Set urls = new HashSet<>();
authorityList.stream().filter(authority ->
!StrUtil.equals(authority.getAuthority(), "ROLE_USER"))
.forEach(authority -> {
Set menuVOSet = menuService.findMenuByRole(authority.getAuthority());
CollUtil.addAll(urls, menuVOSet);
});
urls.stream().filter(menu -> StrUtil.isNotEmpty(menu.getUrl())
&& antPathMatcher.match(menu.getUrl(), request.getRequestURI())
&& request.getMethod().equalsIgnoreCase(menu.getMethod()))
.findFirst().ifPresent(menuVO -> hasPermission.set(true));
}
return hasPermission.get();
}
}
```
### 网关总结
pig 这个系统是个很好的框架,本次体验的是 pig 的 zuul 网关模块,此模块与 feign,ribbon,spring security,Eurasia 进行整合,完成或部分完成了**动态路由**,**灰度发布**, **菜单权限管理**,**服务限流**,**网关日志处理**,非常值得学习!
UPMs 权限管理系统模块
=============
百度了一下,_UPMS_ 是 User Permissions Management System,通用用户权限管理系统
数据库设计
-----
### 部门表

#### 部门关系表

### 字典表
```
/**
* 编号
*/
@TableId(value="id", type= IdType.AUTO)
private Integer id;
/**
* 数据值
*/
private String value;
/**
* 标签名
*/
private String label;
/**
* 类型
*/
private String type;
/**
* 描述
*/
private String description;
/**
* 排序(升序)
*/
private BigDecimal sort;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 更新时间
*/
@TableField("update_time")
private Date updateTime;
/**
* 备注信息
*/
private String remarks;
/**
* 删除标记
*/
@TableField("del_flag")
private String delFlag;
```
### 日志表
```
@Data
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 编号
*/
@TableId(type = IdType.ID_WORKER)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 日志类型
*/
private String type;
/**
* 日志标题
*/
private String title;
/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 操作IP地址
*/
private String remoteAddr;
/**
* 用户代理
*/
private String userAgent;
/**
* 请求URI
*/
private String requestUri;
/**
* 操作方式
*/
private String method;
/**
* 操作提交的数据
*/
private String params;
/**
* 执行时间
*/
private Long time;
/**
* 删除标记
*/
private String delFlag;
/**
* 异常信息
*/
private String exception;
/**
* 服务ID
*/
private String serviceId; }}
```
### 菜单权限表

### 角色表

#### 角色与部门对应关系
略
#### 角色与菜单权限对应关系
略
### 用户表
```
/**
* 主键ID
*/
@TableId(value = "user_id", type = IdType.AUTO)
private Integer userId;
/**
* 用户名
*/
private String username;
private String password;
/**
* 随机盐
*/
@JsonIgnore
private String salt;
/**
* 创建时间
*/
@TableField("create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField("update_time")
private Date updateTime;
/**
* 0-正常,1-删除
*/
@TableField("del_flag")
private String delFlag;
/**
* 简介
*/
private String phone;
/**
* 头像
*/
private String avatar;
/**
* 部门ID
*/
@TableField("dept_id")
private Integer deptId;
```
### 动态路由配置表

业务逻辑
----

全是基于 [mybatis plus](https://mp.baomidou.com/) 的 CRUD,有点多。大部分干这行的都懂,我就不详细展开了。
### 验证码
#### 创建
`ValidateCodeController`可以找到创建验证码相关代码
```
/**
* 创建验证码
*
* @param request request
* @throws Exception
*/
@GetMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{randomStr}")
public void createCode(@PathVariable String randomStr, HttpServletRequest request, HttpServletResponse response)
throws Exception {
Assert.isBlank(randomStr, "机器码不能为空");
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
//生成文字验证码
String text = producer.createText();
//生成图片验证码
BufferedImage image = producer.createImage(text);
userService.saveImageCode(randomStr, text);
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "JPEG", out);
IOUtils.closeQuietly(out);
}
```
其中的 `producer`是使用`Kaptcha`,下面是配置类
```
@Configuration
public class KaptchaConfig {
private static final String KAPTCHA_BORDER = "kaptcha.border";
private static final String KAPTCHA_TEXTPRODUCER_FONT_COLOR = "kaptcha.textproducer.font.color";
private static final String KAPTCHA_TEXTPRODUCER_CHAR_SPACE = "kaptcha.textproducer.char.space";
private static final String KAPTCHA_IMAGE_WIDTH = "kaptcha.image.width";
private static final String KAPTCHA_IMAGE_HEIGHT = "kaptcha.image.height";
private static final String KAPTCHA_TEXTPRODUCER_CHAR_LENGTH = "kaptcha.textproducer.char.length";
private static final Object KAPTCHA_IMAGE_FONT_SIZE = "kaptcha.textproducer.font.size";
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put(KAPTCHA_BORDER, SecurityConstants.DEFAULT_IMAGE_BORDER);
properties.put(KAPTCHA_TEXTPRODUCER_FONT_COLOR, SecurityConstants.DEFAULT_COLOR_FONT);
properties.put(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, SecurityConstants.DEFAULT_CHAR_SPACE);
properties.put(KAPTCHA_IMAGE_WIDTH, SecurityConstants.DEFAULT_IMAGE_WIDTH);
properties.put(KAPTCHA_IMAGE_HEIGHT, SecurityConstants.DEFAULT_IMAGE_HEIGHT);
properties.put(KAPTCHA_IMAGE_FONT_SIZE, SecurityConstants.DEFAULT_IMAGE_FONT_SIZE);
properties.put(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, SecurityConstants.DEFAULT_IMAGE_LENGTH);
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
```
#### 发送手机验证码
大体逻辑为,先查询验证码 redis 缓存,没有缓存则说明验证码缓存没有失效,返回错误。
查到没有验证码,则根据手机号码从数据库获得用户信息,生成一个 4 位的验证码,使用`rabbbitmq`队列把短信验证码保存到队列,同时加上手机验证码的 redis 缓存
```
/**
* 发送验证码
*
* 1. 先去redis 查询是否 60S内已经发送
* 2. 未发送: 判断手机号是否存 ? false :产生4位数字 手机号-验证码
* 3. 发往消息中心-》发送信息
* 4. 保存redis
*
* @param mobile 手机号
* @return true、false
*/
@Override
public R sendSmsCode(String mobile) {
Object tempCode = redisTemplate.opsForValue().get(SecurityConstants.DEFAULT_CODE_KEY + mobile);
if (tempCode != null) {
log.error("用户:{}验证码未失效{}", mobile, tempCode);
return new R<>(false, "验证码未失效,请失效后再次申请");
}
SysUser params = new SysUser();
params.setPhone(mobile);
List userList = this.selectList(new EntityWrapper<>(params));
if (CollectionUtil.isEmpty(userList)) {
log.error("根据用户手机号{}查询用户为空", mobile);
return new R<>(false, "手机号不存在");
}
String code = RandomUtil.randomNumbers(4);
JSONObject contextJson = new JSONObject();
contextJson.put("code", code);
contextJson.put("product", "Pig4Cloud");
log.info("短信发送请求消息中心 -> 手机号:{} -> 验证码:{}", mobile, code);
rabbitTemplate.convertAndSend(MqQueueConstant.MOBILE_CODE_QUEUE,
new MobileMsgTemplate(
mobile,
contextJson.toJSONString(),
CommonConstant.ALIYUN_SMS,
EnumSmsChannelTemplate.LOGIN_NAME_LOGIN.getSignName(),
EnumSmsChannelTemplate.LOGIN_NAME_LOGIN.getTemplate()
));
redisTemplate.opsForValue().set(SecurityConstants.DEFAULT_CODE_KEY + mobile, code, SecurityConstants.DEFAULT_IMAGE_EXPIRE, TimeUnit.SECONDS);
return new R<>(true);
}
```
### 树形节点工具栏
```
public class TreeUtil {
/**
* 两层循环实现建树
*
* @param treeNodes 传入的树节点列表
* @return
*/
public static List bulid(List treeNodes, Object root) {
List trees = new ArrayList();
for (T treeNode : treeNodes) {
if (root.equals(treeNode.getParentId())) {
trees.add(treeNode);
}
for (T it : treeNodes) {
if (it.getParentId() == treeNode.getId()) {
if (treeNode.getChildren() == null) {
treeNode.setChildren(new ArrayList());
}
treeNode.add(it);
}
}
}
return trees;
}
/**
* 使用递归方法建树
*
* @param treeNodes
* @return
*/
public static List buildByRecursive(List treeNodes, Object root) {
List trees = new ArrayList();
for (T treeNode : treeNodes) {
if (root.equals(treeNode.getParentId())) {
trees.add(findChildren(treeNode, treeNodes));
}
}
return trees;
}
/**
* 递归查找子节点
*
* @param treeNodes
* @return
*/
public static T findChildren(T treeNode, List treeNodes) {
for (T it : treeNodes) {
if (treeNode.getId() == it.getParentId()) {
if (treeNode.getChildren() == null) {
treeNode.setChildren(new ArrayList());
}
treeNode.add(findChildren(it, treeNodes));
}
}
return treeNode;
}
/**
* 通过sysMenu创建树形节点
*
* @param menus
* @param root
* @return
*/
public static List bulidTree(List menus, int root) {
List trees = new ArrayList();
MenuTree node;
for (SysMenu menu : menus) {
node = new MenuTree();
node.setId(menu.getMenuId());
node.setParentId(menu.getParentId());
node.setName(menu.getName());
node.setUrl(menu.getUrl());
node.setPath(menu.getPath());
node.setCode(menu.getPermission());
node.setLabel(menu.getName());
node.setComponent(menu.getComponent());
node.setIcon(menu.getIcon());
trees.add(node);
}
return TreeUtil.bulid(trees, root);
}
}
```
### 生成 avue 模板类
```
public class PigResourcesGenerator {
public static void main(String[] args) {
String outputDir = "/Users/lengleng/work/temp";
final String viewOutputDir = outputDir + "/view/";
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(outputDir);
gc.setFileOverride(true);
gc.setActiveRecord(true);
// XML 二级缓存
gc.setEnableCache(false);
// XML ResultMap
gc.setBaseResultMap(true);
// XML columList
gc.setBaseColumnList(true);
gc.setAuthor("lengleng");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDbType(DbType.MYSQL);
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("lengleng");
dsc.setUrl("jdbc:mysql://139.224.200.249:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false");
mpg.setDataSource(dsc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
// strategy.setCapitalMode(true);// 全局大写命名 ORACLE 注意
strategy.setSuperControllerClass("com.github.pig.common.web.BaseController");
// 表名生成策略
strategy.setNaming(NamingStrategy.underline_to_camel);
mpg.setStrategy(strategy);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.github.pig.admin");
pc.setController("controller");
mpg.setPackageInfo(pc);
// 注入自定义配置,可以在 VM 中使用 cfg.abc 设置的值
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
}
};
// 生成的模版路径,不存在时需要先新建
File viewDir = new File(viewOutputDir);
if (!viewDir.exists()) {
viewDir.mkdirs();
}
List focList = new ArrayList();
focList.add(new FileOutConfig("/templates/listvue.vue.vm") {
@Override
public String outputFile(TableInfo tableInfo) {
return getGeneratorViewPath(viewOutputDir, tableInfo, ".vue");
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
//生成controller相关
mpg.execute();
}
/**
* 获取配置文件
*
* @return 配置Props
*/
private static Properties getProperties() {
// 读取配置文件
Resource resource = new ClassPathResource("/config/application.properties");
Properties props = new Properties();
try {
props = PropertiesLoaderUtils.loadProperties(resource);
} catch (IOException e) {
e.printStackTrace();
}
return props;
}
/**
* 页面生成的文件名
*/
private static String getGeneratorViewPath(String viewOutputDir, TableInfo tableInfo, String suffixPath) {
String name = StringUtils.firstToLowerCase(tableInfo.getEntityName());
String path = viewOutputDir + "/" + name + "/index" + suffixPath;
File viewDir = new File(path).getParentFile();
if (!viewDir.exists()) {
viewDir.mkdirs();
}
return path;
}
}
```
velocity 模板
```
package $!{package.Controller};
import java.util.Map;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.github.pig.common.constant.CommonConstant;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.github.pig.common.util.Query;
import com.github.pig.common.util.R;
import $!{package.Entity}.$!{entity};
import $!{package.Service}.$!{entity}Service;
#if($!{superControllerClassPackage})
import $!{superControllerClassPackage};
#end
/**
*
* $!{table.comment} 前端控制器
*
*
* @author $!{author}
* @since $!{date}
*/
@RestController
@RequestMapping("/$!{table.entityPath}")
public class $!{table.controllerName} extends $!{superControllerClass} {
@Autowired private $!{entity}Service $!{table.entityPath}Service;
/**
* 通过ID查询
*
* @param id ID
* @return $!{entity}
*/
@GetMapping("/{id}")
public R<$!{entity}> get(@PathVariable Integer id) {
return new R<>($!{table.entityPath}Service.selectById(id));
}
/**
* 分页查询信息
*
* @param params 分页对象
* @return 分页对象
*/
@RequestMapping("/page")
public Page page(@RequestParam Map params) {
params.put(CommonConstant.DEL_FLAG, CommonConstant.STATUS_NORMAL);
return $!{table.entityPath}Service.selectPage(new Query<>(params), new EntityWrapper<>());
}
/**
* 添加
* @param $!{table.entityPath} 实体
* @return success/false
*/
@PostMapping
public R add(@RequestBody $!{entity} $!{table.entityPath}) {
return new R<>($!{table.entityPath}Service.insert($!{table.entityPath}));
}
/**
* 删除
* @param id ID
* @return success/false
*/
@DeleteMapping("/{id}")
public R delete(@PathVariable Integer id) {
$!{entity} $!{table.entityPath} = new $!{entity}();
$!{table.entityPath}.setId(id);
$!{table.entityPath}.setUpdateTime(new Date());
$!{table.entityPath}.setDelFlag(CommonConstant.STATUS_DEL);
return new R<>($!{table.entityPath}Service.updateById($!{table.entityPath}));
}
/**
* 编辑
* @param $!{table.entityPath} 实体
* @return success/false
*/
@PutMapping
public R edit(@RequestBody $!{entity} $!{table.entityPath}) {
$!{table.entityPath}.setUpdateTime(new Date());
return new R<>($!{table.entityPath}Service.updateById($!{table.entityPath}));
}
}
```
缓存
--
在部分实现类中,我们看到了作者使用了`spring cache`相关的注解。现在我们回忆一下相关缓存注解的含义:

`@Cacheable`: 用来定义缓存的。常用到是 value,key; 分别用来指明缓存的名称和方法中参数,对于 value 你也可以使用 cacheName,在查看源代码是我们可以看到:两者是指的同一个东西。
`@CacheEvict`: 用来清理缓存。常用有 cacheNames,allEntries(默认值 false);分别代表了要清除的缓存名称和是否全部清除 (true 代表全部清除)。
`@CachePut`: 用来更新缓存,用它来注解的方法都会被执行,执行完后结果被添加到缓存中。该方法不能和 @Cacheable 同时在同一个方法上使用。
后台跑批定时任务模块
==========
`Elastic-Job`是 ddframe 中 dd-job 的作业模块中分离出来的分布式弹性作业框架。去掉了和 dd-job 中的监控和 ddframe 接入规范部分。该项目基于成熟的开源产品 Quartz 和 Zookeeper 及其客户端 Curator 进行二次开发。主要功能如下:
* **定时任务:** 基于成熟的定时任务作业框架 Quartz cron 表达式执行定时任务。
* **作业注册中心:** 基于 Zookeeper 和其客户端 Curator 实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。
* **作业分片:** 将一个任务分片成为多个小任务项在多服务器上同时执行。
* **弹性扩容缩容:** 运行中的作业服务器崩溃,或新增加 n 台作业服务器,作业框架将在下次作业执行前重新分片,不影响当前作业执行。
* **支持多种作业执行模式:** 支持 OneOff,Perpetual 和 SequencePerpetual 三种作业模式。
* **失效转移:** 运行中的作业服务器崩溃不会导致重新分片,只会在下次作业启动时分片。启用失效转移功能可以在本次作业执行过程中,监测其他作业服务器空闲,抓取未完成的孤儿分片项执行。
* **运行时状态收集:** 监控作业运行时状态,统计最近一段时间处理的数据成功和失败数量,记录作业上次运行开始时间,结束时间和下次运行时间。
* ** 作业停止,恢复和禁用:** 用于操作作业启停,并可以禁止某作业运行(上线时常用)。
* ** 被错过执行的作业重触发:** 自动记录错过执行的作业,并在上次作业完成后自动触发。可参考 Quartz 的 misfire。
* ** 多线程快速处理数据:** 使用多线程处理抓取到的数据,提升吞吐量。
* ** 幂等性:** 重复作业任务项判定,不重复执行已运行的作业任务项。由于开启幂等性需要监听作业运行状态,对瞬时反复运行的作业对性能有较大影响。
* ** 容错处理:** 作业服务器与 Zookeeper 服务器通信失败则立即停止作业运行,防止作业注册中心将失效的分片分项配给其他作业服务器,而当前作业服务器仍在执行任务,导致重复执行。
* **Spring 支持:** 支持 spring 容器,自定义命名空间,支持占位符。
* ** 运维平台:** 提供运维界面,可以管理作业和注册中心。
配置
--
作者直接使用了开源项目的配置,我顺着他的 pom 文件找到了这家的 github,地址如下
[github.com/xjzrc/elast…](https://github.com/xjzrc/elastic-job-lite-spring-boot-starter)
### 工作流作业配置
```
@ElasticJobConfig(cron = "0 0 0/1 * * ? ", shardingTotalCount = 3, shardingItemParameters = "0=Beijing,1=Shanghai,2=Guangzhou")
public class PigDataflowJob implements DataflowJob {
@Override
public List fetchData(ShardingContext shardingContext) {
return null;
}
@Override
public void processData(ShardingContext shardingContext, List list) {
}
}
```
### 测试代码
```
@Slf4j
@ElasticJobConfig(cron = "0 0 0/1 * * ?", shardingTotalCount = 3,
shardingItemParameters = "0=pig1,1=pig2,2=pig3",
startedTimeoutMilliseconds = 5000L,
completedTimeoutMilliseconds = 10000L,
eventTraceRdbDataSource = "dataSource")
public class PigSimpleJob implements SimpleJob {
/**
* 业务执行逻辑
*
* @param shardingContext 分片信息
*/
@Override
public void execute(ShardingContext shardingContext) {
log.info("shardingContext:{}", shardingContext);
}
}
```
开源版对这个支持有限,等到拿到收费版我在做分析。
消息中心
====
这里的消息中心主要是集成了钉钉服务和阿里大鱼短息服务
钉钉
--
### 配置
钉钉是相当简单了,只需要一个`webhook`信息就够了。
`webhook`是一种 web 回调或者 http 的 push API,是向 APP 或者其他应用提供实时信息的一种方式。Webhook 在数据产生时立即发送数据,也就是你能实时收到数据。这一种不同于典型的 API,需要用了实时性需要足够快的轮询。这无论是对生产还是对消费者都是高效的,唯一的缺点是初始建立困难。Webhook 有时也被称为反向 API,因为他提供了 API 规则,你需要设计要使用的 API。Webhook 将向你的应用发起 http 请求,典型的是 post 请求,应用程序由请求驱动。
```
@Data
@Configuration
@ConfigurationProperties(prefix = "sms.dingtalk")
public class DingTalkPropertiesConfig {
/**
* webhook
*/
private String webhook;
}
```
### 消息模板
```
/**
* @author lengleng
* @date 2018/1/15
* 钉钉消息模板
* msgtype : text
* text : {"content":"服务: pig-upms-service 状态:UP"}
*/
@Data
@ToString
public class DingTalkMsgTemplate implements Serializable {
private String msgtype;
private TextBean text;
public String getMsgtype() {
return msgtype;
}
public void setMsgtype(String msgtype) {
this.msgtype = msgtype;
}
public TextBean getText() {
return text;
}
public void setText(TextBean text) {
this.text = text;
}
public static class TextBean {
/**
* content : 服务: pig-upms-service 状态:UP
*/
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
}
```
### 监听
使用队列时时监听
```
@Slf4j
@Component
@RabbitListener(queues = MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE)
public class DingTalkServiceChangeReceiveListener {
@Autowired
private DingTalkMessageHandler dingTalkMessageHandler;
@RabbitHandler
public void receive(String text) {
long startTime = System.currentTimeMillis();
log.info("消息中心接收到钉钉发送请求-> 内容:{} ", text);
dingTalkMessageHandler.process(text);
long useTime = System.currentTimeMillis() - startTime;
log.info("调用 钉钉网关处理完毕,耗时 {}毫秒", useTime);
}
}
```
### 发送
使用队列发送
```
@Slf4j
@Component
public class DingTalkMessageHandler {
@Autowired
private DingTalkPropertiesConfig dingTalkPropertiesConfig;
/**
* 业务处理
*
* @param text 消息
*/
public boolean process(String text) {
String webhook = dingTalkPropertiesConfig.getWebhook();
if (StrUtil.isBlank(webhook)) {
log.error("钉钉配置错误,webhook为空");
return false;
}
DingTalkMsgTemplate dingTalkMsgTemplate = new DingTalkMsgTemplate();
dingTalkMsgTemplate.setMsgtype("text");
DingTalkMsgTemplate.TextBean textBean = new DingTalkMsgTemplate.TextBean();
textBean.setContent(text);
dingTalkMsgTemplate.setText(textBean);
String result = HttpUtil.post(webhook, JSONObject.toJSONString(dingTalkMsgTemplate));
log.info("钉钉提醒成功,报文响应:{}", result);
return true;
}
}
```
阿里大鱼短息服务
--------
### 配置
```
@Data
@Configuration
@ConditionalOn")
@ConfigurationProperties(prefix = "sms.aliyun")
public class SmsAliyunPropertiesConfig {
/**
* 应用ID
*/
private String accessKey;
/**
* 应用秘钥
*/
private String secretKey;
/**
* 短信模板配置
*/
private Map channels;
}
```
### 监听
```
@Slf4j
@Component
@RabbitListener(queues = MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE)
public class MobileServiceChangeReceiveListener {
@Autowired
private Map messageHandlerMap;
@RabbitHandler
public void receive(MobileMsgTemplate mobileMsgTemplate) {
long startTime = System.currentTimeMillis();
log.info("消息中心接收到短信发送请求-> 手机号:{} -> 信息体:{} ", mobileMsgTemplate.getMobile(), mobileMsgTemplate.getContext());
String channel = mobileMsgTemplate.getChannel();
SmsMessageHandler messageHandler = messageHandlerMap.get(channel);
if (messageHandler == null) {
log.error("没有找到指定的路由通道,不进行发送处理完毕!");
return;
}
messageHandler.execute(mobileMsgTemplate);
long useTime = System.currentTimeMillis() - startTime;
log.info("调用 {} 短信网关处理完毕,耗时 {}毫秒", mobileMsgTemplate.getType(), useTime);
}
}
```
### 发送
不错的模板
```
@Slf4j
@Component(CommonConstant.ALIYUN_SMS)
public class SmsAliyunMessageHandler extends AbstractMessageHandler {
@Autowired
private SmsAliyunPropertiesConfig smsAliyunPropertiesConfig;
private static final String PRODUCT = "Dysmsapi";
private static final String DOMAIN = "dysmsapi.aliyuncs.com";
/**
* 数据校验
*
* @param mobileMsgTemplate 消息
*/
@Override
public void check(MobileMsgTemplate mobileMsgTemplate) {
Assert.isBlank(mobileMsgTemplate.getMobile(), "手机号不能为空");
Assert.isBlank(mobileMsgTemplate.getContext(), "短信内容不能为空");
}
/**
* 业务处理
*
* @param mobileMsgTemplate 消息
*/
@Override
public boolean process(MobileMsgTemplate mobileMsgTemplate) {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsAliyunPropertiesConfig.getAccessKey(), smsAliyunPropertiesConfig.getSecretKey());
try {
DefaultProfile.addEndpoint("cn-hou", "cn-hangzhou", PRODUCT, DOMAIN);
} catch (ClientException e) {
log.error("初始化SDK 异常", e);
e.printStackTrace();
}
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers(mobileMsgTemplate.getMobile());
//必填:短信签名-可在短信控制台中找到
request.setSignName(mobileMsgTemplate.getSignName());
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode(smsAliyunPropertiesConfig.getChannels().get(mobileMsgTemplate.getTemplate()));
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"
request.setTemplateParam(mobileMsgTemplate.getContext());
request.setOutId(mobileMsgTemplate.getMobile());
//hint 此处可能会抛出异常,注意catch
try {
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
log.info("短信发送完毕,手机号:{},返回状态:{}", mobileMsgTemplate.getMobile(), sendSmsResponse.getCode());
} catch (ClientException e) {
log.error("发送异常");
e.printStackTrace();
}
return true;
}
/**
* 失败处理
*
* @param mobileMsgTemplate 消息
*/
@Override
public void fail(MobileMsgTemplate mobileMsgTemplate) {
log.error("短信发送失败 -> 网关:{} -> 手机号:{}", mobileMsgTemplate.getType(), mobileMsgTemplate.getMobile());
}
}
```
资源认证服务器 (单点登陆功能)
================
由于作者在认证中心使用了 spring security oauth 框架,所以需要在微服务的客户端实现一个资源认证服务器,来完成 SSO 需求。
配置
--
暴露监控信息
```
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
```
接口
--
```
@EnableOAuth2Sso
@SpringBootApplication
public class PigSsoClientDemoApplication {
public static void main(String[] args) {
SpringApplication.run(PigSsoClientDemoApplication.class, args);
}
}
```
监控模块
====
springboot admin 配置
-------------------
`RemindingNotifier`会在应用上线或宕掉的时候发送提醒,也就是把`notifications`发送给其他的`notifier`,notifier 的实现很有意思,不深究了,从类关系可以知道,我们可以以这么几种方式发送 notifications:Pagerduty、Hipchat 、Slack 、Mail、 Reminder
```
@Configuration
public static class NotifierConfig {
@Bean
@Primary
public RemindingNotifier remindingNotifier() {
RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(loggerNotifier()));
notifier.setReminderPeriod(TimeUnit.SECONDS.toMillis(10));
return notifier;
}
@Scheduled(fixedRate = 1_000L)
public void remind() {
remindingNotifier().sendReminders();
}
@Bean
public FilteringNotifier filteringNotifier(Notifier delegate) {
return new FilteringNotifier(delegate);
}
@Bean
public LoggingNotifier loggerNotifier() {
return new LoggingNotifier();
}
}
```
### 短信服务下线通知
继承`AbstractStatusChangeNotifier`,将短信服务注册到`spring boot admin`中。
```
@Slf4j
public class StatusChangeNotifier extends AbstractStatusChangeNotifier {
private RabbitTemplate rabbitTemplate;
private MonitorPropertiesConfig monitorMobilePropertiesConfig;
public StatusChangeNotifier(MonitorPropertiesConfig monitorMobilePropertiesConfig, RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
this.monitorMobilePropertiesConfig = monitorMobilePropertiesConfig;
}
/**
* 通知逻辑
*
* @param event 事件
* @throws Exception 异常
*/
@Override
protected void doNotify(ClientApplicationEvent event) {
if (event instanceof ClientApplicationStatusChangedEvent) {
log.info("Application {} ({}) is {}", event.getApplication().getName(),
event.getApplication().getId(), ((ClientApplicationStatusChangedEvent) event).getTo().getStatus());
String text = String.format("应用:%s 服务ID:%s 状态改变为:%s,时间:%s"
, event.getApplication().getName()
, event.getApplication().getId()
, ((ClientApplicationStatusChangedEvent) event).getTo().getStatus()
, DateUtil.date(event.getTimestamp()).toString());
JSONObject contextJson = new JSONObject();
contextJson.put("name", event.getApplication().getName());
contextJson.put("seid", event.getApplication().getId());
contextJson.put("time", DateUtil.date(event.getTimestamp()).toString());
//开启短信通知
if (monitorMobilePropertiesConfig.getMobile().getEnabled()) {
log.info("开始短信通知,内容:{}", text);
rabbitTemplate.convertAndSend(MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE,
new MobileMsgTemplate(
CollUtil.join(monitorMobilePropertiesConfig.getMobile().getMobiles(), ","),
contextJson.toJSONString(),
CommonConstant.ALIYUN_SMS,
EnumSmsChannelTemplate.SERVICE_STATUS_CHANGE.getSignName(),
EnumSmsChannelTemplate.SERVICE_STATUS_CHANGE.getTemplate()
));
}
if (monitorMobilePropertiesConfig.getDingTalk().getEnabled()) {
log.info("开始钉钉通知,内容:{}", text);
rabbitTemplate.convertAndSend(MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE, text);
}
} else {
log.info("Application {} ({}) {}", event.getApplication().getName(),
event.getApplication().getId(), event.getType());
}
}
}
```
zipkin 链路追踪
===========
由于 zipkin 是侵入式,因此这部分组件没有代码,只有相关依赖。下面分享一下作者的 yaml
DB
--
```
server:
port: 5003
# datasoure默认使用JDBC
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: ENC(gc16brBHPNq27HsjaULgKGq00Rz6ZUji)
url: jdbc:mysql://127.0.0.1:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
zipkin:
collector:
rabbitmq:
addresses: 127.0.0.1:5682
password: lengleng
username: pig
queue: zipkin
storage:
type: mysql
```
ELK
---
```
server:
port: 5002
zipkin:
collector:
rabbitmq:
addresses: 127.0.0.1:5682
password: lengleng
username: pig
queue: zipkin
storage:
type: elasticsearch
elasticsearch:
hosts: 127.0.0.1:9200
cluster: elasticsearch
index: zipkin
max-requests: 64
index-shards: 5
index-replicas: 1
```
续 1s 时间
=======
全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的?个人[博客](https://blog.tengshe789.tech/) ?吧!
谢谢你那么可爱,还一直关注着我~❤?