该项目基于 SpringCloud 搭建的一个电商系统,采用前后端分离微服务架构,实现了基于 RBAC/JWT 权限认证的解决方案,集成了各种微服务治理和监控功能。模块包括:业务逻辑层管理、 管理员角色权限系统、应用监控、Druid 数据库监控、日志系统(ELK)、配置中心、Swagger 文档、任务调度等。
用户端目前只做了小程序,安卓、IOS端APP可以直接使用接口来开发。
架构图如下:
小程序端
Nacos配置中心
Grafana监控系统
Kibana日志系统
Druid监控系统
Swagger文档
系统模块
leaf-admin:总后台管理系统
leaf-business:商家管理系统
leaf-client:用户端,小程序、app的使用接口
leaf-common:公共模块系统,里面放了一些公告模块和公共类比如:swagger、lombok、分页工具、公共返回结果类、错误代码、公共异常处理等
leaf-gateway:网关
leaf-mbg:使用mybatis和generator自动生成的Entity实体和mapper数据库访问层
leaf-order:订单服务
leaf-score:积分服务
leaf-security:使用spring security封装的一个验证、鉴权模块,实现了使用jwt验证的和RBAC基于角色的访问控制功能
使用 Security 结合 JWT 实现 url 级的权限控制系统,使用 Generator/Mybatis 自动生成数据库操作 的基本方法,数据库连接池使用 Druid 配置多个数据源,并实现了读写分离。日志系统通过 Logback 写到 Redis 队列中供 Logstash 读取,使用 Prometheus、Grafana 对 Redis、Mysql 和微服务进行监控。使用 Redis 实现了分布式锁和分布式事务(TX-LCN/Seata/RocketMQ), 使用 Sentinel 实现服务降级、熔断、限流和监控。
一、leaf-common模块
pom文件依赖了swagger2、lombok这些公共模块,不用重复依赖。
CommonPage类:分页的公共处理对象,封装了pagehelper的总页数、当前页等信息
CommonResult类:公共的结果返回对象,定义了返回的code、message、data字段和封装了一些常用的静态返回方法,比如在控制器中直接使用return CommonResult.success(data);返回结果。
ApiException:定义了我们自己的异常处理类,继承自RuntimeException类,然后创建一个类GlobalExceptionHandler添加全局异常处理类使用注解@GlobalExceptionHandler代码如下:
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
这样只要在项目中抛出ApiException异常都会被handle捕获,返回通用的CommonResult结果。
还定义了断言类Asserts、返回错误码ResultCode等。
二、leaf-security
这个模块使用spring security封装的一个验证、鉴权的功能,它使用了spring security库。关于security我之前写过一篇介绍原理的博客分析Spring Security实现方式,在这里我只说一下它的应用,在日常的用户登录有以下场景:
一、用户发送username和password到服务器,控制器调用service层去数据库进行账号密码的验证;
二、如果验证成功,把用户信息封装成一个UserDetails使用jwt工具类生成一个token返还给客户端,类似这样eyJhbGciOiJIUzUxMiJ9.eyJuYW1lIjoi6a2P5Lqa5oGSIiwiZXhwIjoxNTg5Nzc2ODMzLCJhZ2UiOjEyM30.J0WvlpSvuS0OIyEpS4uEMqqO2PCWWkWLkFmQKX2l4vl-vVNTpdbiTcNTEj8qX3EFBZUYOIBLfgacaKMH4dCmAQ红色部分是JWT的头部,它定义这个token的加密算法类型,之后使用base64进行编码。黄色部分记录了信息比如用户名、过期时间等然后进行base64编码,最后绿色是对前面两部分内容进行sha256算法签名,秘钥在服务器可以有效防止数据被篡改。关于JWT的介绍可以看这篇文章JSON Web Token;
三、用户端这时候拿到token可以放到cookie中也可以放到本地存储器中,在访问服务器时把它放在HTTP的Header头中。
四、项目使用了security,所以请求到达控制器之前会先通过UsernamePasswordAuthenticationFilter过滤器,我们在UsernamePasswordAuthenticationFilter过滤器之前添加一个自己的过滤器jwtAuthenticationTokenFilter,在这个过滤器中我们验证这个JWT的签名是否合法,并且拿出里面的username,如果这个username在数据库中存在(因为操作数据库频率高,所以使用redis做了缓存),我们拿到用户UserDetails,然后创建一个UsernamePasswordAuthenticationToken实例authentication,再把authentication放到SecurityContextHolder中,这时候security就会从SecurityContext中拿到这个有效的authentication进行验证了。
五、之后就是角色鉴权的流程了,本文主要介绍系统的整体架构,由于篇幅原因关于角色鉴权具体实现可以看我之前的文章分析Spring Security实现方式进行了解。
这里贴一下重要类的实现及配置,关于SecurityConfig的配置类,代码如下:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// authorizeRequests方法 定义哪些URL需要被保护、哪些不需要被保护
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
//允许跨域请求的OPTIONS请求
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
// 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类 添加自定义未授权和未登录结果返回
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//有动态权限配置时添加动态权限校验过滤器
if(dynamicSecurityService!=null){
registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
return new RestfulAccessDeniedHandler();
}
@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
@Bean
public IgnoreUrlsConfig ignoreUrlsConfig() {
return new IgnoreUrlsConfig();
}
@Bean
public JwtTokenUtil jwtTokenUtil() {
return new JwtTokenUtil();
}
@ConditionalOnBean(name = "dynamicSecurityService")
@Bean
public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
return new DynamicAccessDecisionManager();
}
@ConditionalOnBean(name = "dynamicSecurityService")
@Bean
public DynamicSecurityFilter dynamicSecurityFilter() {
return new DynamicSecurityFilter();
}
@ConditionalOnBean(name = "dynamicSecurityService")
@Bean
public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
return new DynamicSecurityMetadataSource();
}
}
简单解释下上面代码的功能
1、configure(HttpSecurity httpSecurity):对Security的一些配置,比如用于需要拦截的url路径、jwt过滤器及出异常后的处理器等配置;
2、 configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
3、JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录。
权限认证最主要的是JwtAuthenticationTokenFilter该类继承了OncePerRequestFilter过滤器,保证在请求中只执行一次。代码如下:
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
至此security的验证和鉴权讲述完毕,还有一些security公共的鉴权失败返回类可以看看之前的文章,这里不在赘述。
三、leaf-mbg模块
这个模块主要使用generator自动生成一些操作数据库的重复性代码,只需要配置一下generator就会自动把数据库表生成model和mapper极大的减少了和数据库增删改查这类重复性的工作。我贴一下generator的配置信息:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- defaultModelType="flat" 设置复合主键时不单独为主键创建实体 -->
<context id="MySql" defaultModelType="flat">
<!-- 生成的POJO实现java.io.Serializable接口 -->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin" />
<!--注释-->
<commentGenerator>
<!-- 将数据库中表的字段描述信息添加到注释 -->
<property name="addRemarkComments" value="true"/>
<!-- 注释里不添加日期 -->
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- 数据库连接 -->
<jdbcConnection
driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/leaf"
userId="root"
password="root"/>
<!-- 生成POJO对象,并将类放到com.songguoliang.springboot.entity包下 -->
<javaModelGenerator targetPackage="com.funheng.leaf.model" targetProject="src/main/java"></javaModelGenerator>
<!-- 生成mapper xml文件,并放到resources下的mapper文件夹下 -->
<sqlMapGenerator targetPackage="com.funheng.leaf.mapper" targetProject="src/main/resources"></sqlMapGenerator>
<!-- 生成mapper xml对应dao接口,放到com.songguoliang.springboot.mapper包下-->
<javaClientGenerator targetPackage="com.funheng.leaf.mapper" targetProject="src/main/java" type="XMLMAPPER"></javaClientGenerator>
<!-- table标签可以有多个,至少一个,tableName指定表名,可以使用_和%通配符 -->
<table tableName="%">
<!-- 是否只生成POJO对象 -->
<property name="modelOnly" value="false"/>
<!-- 数据库中表名有时我们都会带个前缀,而实体又不想带前缀,这个配置可以把实体的前缀去掉 -->
<!--<domainObjectRenamingRule searchString="^Tbl" replaceString=""/>-->
<!-- 插入后返回ID -->
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
</context>
</generatorConfiguration>
上面3个模块都是功能架构的基础模块,后面的三个模块都是涉及到具体业务逻辑功能了
四、leaf-admin
这个是总后台模块,依赖了以上的leaf-common、leaf-mbg、leaf-security三个模块,实现了对整个系统功能的管理,如会员管理、商家管理、订单管理等等,并且基于security实现了后台的RBAC管理员角色控制权限系统,可以基于角色对每个模块下的不同功能进行权限分配,该部分只提供REST接口,前端使用VUE编写的一个单页应用对后台数据的一个展现和操作。
目录如下
component是定义了一些功能组件,比如切面类放到里面,某些全局工具也可以放到里面。
config放了一些配置类,比如全局跨域配置、security权限配置、mybatis扫描配置、Swagger配置、Oss配置等。
controller、service、dao、dto不多说了就是控制器层、业务层、数据层和模型。
validator里面放了自定义的验证规则,spring boot在post接收一个对象参数的时候可以使用@Validated进行验证,比如在模型字段上使用@NotNull(限制必须不为null)、@Max(value)限制必须为一个不大于指定值的数字)、@NotEmpty(验证注解的元素值不为空)等
如果有不满足自己验证格式的可以创建注解并且使用@Constraint(validatedBy = {MyConstraintValidator.class})给注解分配验证的类。
下面我简单说一下每个包里面的内容和功能
1、component
里面放了WebLogAspect这个切面类,对控制器里面所有方法定义了切点。
@Pointcut("execution(public * com.funheng.leaf.controller.*.*(..))")
public void webLog() {
}
在环绕通知里面使用Object result = joinPoint.proceed();获取控制器返回的结果
使用Signature signature = joinPoint.getSignature(); 获取通知的签名,Method method = methodSignature.getMethod();获取方法通过反射得到swagger的注解。通过joinPoint.getArgs()得到参数信息,然后把这些信息整理输入到日志中,对所有的接口访问进行参数、返回结果的日志记录。
代码如下:
/**
* 日志处理切面
*/
@Aspect
@Component
public class WebLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
/**
* 定义切面
* 切点表达式: execution(...)
* 注解表达式: @annotation(com.space.aspect.anno.SysLog)
*/
@Pointcut("execution(public * com.funheng.leaf.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
// 环绕通知 @Around , 当然也可以使用 @Before (前置通知) @After (后置通知)
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录请求信息(通过Logstash传入Elasticsearch)
WebLog webLog = new WebLog();
Object result = joinPoint.proceed();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(ApiOperation.class)) {
ApiOperation log = method.getAnnotation(ApiOperation.class);
webLog.setDescription(log.value());
}
long endTime = System.currentTimeMillis();
String urlStr = request.getRequestURL().toString();
webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
webLog.setIp(request.getRemoteUser());
webLog.setMethod(request.getMethod());
webLog.setParameter(getParameter(method, joinPoint.getArgs()));
webLog.setResult(result);
webLog.setSpendTime((int) (endTime - startTime));
webLog.setStartTime(startTime);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
Map<String,Object> logMap = new HashMap<>();
logMap.put("url",webLog.getUrl());
logMap.put("method",webLog.getMethod());
logMap.put("parameter",webLog.getParameter());
logMap.put("spendTime",webLog.getSpendTime());
logMap.put("description",webLog.getDescription());
LOGGER.info(Markers.appendEntries(logMap), JSONUtil.parse(webLog).toString());
return result;
}
/**
* 根据方法和传入的参数获取请求参数
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
//将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
//将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
}
2、config
里面有
含有5个配置类,依次是security权限配置、全局跨域配置、mybatis扫描配置、Oss配置、Swagger配置。
AdminSecurityConfig,他继承leaf-security中的SecurityConfig,拥有了security的权限验证,但是需要配置自己的
userDetailsService和动态资源权限信息dynamicSecurityService,userDetailsService会在JwtAuthenticationTokenFilter过滤器中被使用来获得用户实例,这里用到了个匿名函数的注入,dynamicSecurityService是从数据库中拿到权限配置。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class AdminSecurityConfig extends SecurityConfig {
@Autowired
private UmsAdminService adminService;
@Autowired
private UmsResourceService resourceService;
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> adminService.loadUserByUsername(username);
}
// 动态权限数据源获取 注入到 动态权限数据源中(DynamicSecurityMetadataSource)
@Bean
public DynamicSecurityService dynamicSecurityService() {
return new DynamicSecurityService() {
@Override
public Map<String, ConfigAttribute> loadDataSource() {
Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
List<UmsResource> resourceList = resourceService.listAll();
for (UmsResource resource : resourceList) {
map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
}
return map;
}
};
}
}
全局跨域配置是解决小程序或者VUE这种单页面应用在浏览器中使用ajax发送网络请求被拒绝的问题,配置很简单:
@Configuration
public class GlobalCorsConfig {
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许所有域名进行跨域调用
config.addAllowedOrigin("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
MyBatisConfig只使用了@MapperScan({"com.funheng.leaf.mapper","com.funheng.leaf.dao"})注解来加载这些包
OssConfig是阿里云的一个Oss配置
Swagger2Config是一个文档配置,在控制器,模型中使用@Api、@ApiOperation、@ApiModelProperty等注解自动生成接口文档
3、controller
@RestController
@Api(tags = "BsnBusinessController", description = "商家管理")
@RequestMapping("/business")
public class BsnBusinessController {
@Autowired
BsnBusinessService bsnBusinessService;
@ApiOperation("创建商家")
@RequestMapping(value = "/create", method = RequestMethod.POST)
public CommonResult create(@Validated @RequestBody BsnBusinessParam bsnBusinessParam) {
int result = bsnBusinessService.create(bsnBusinessParam);
if (result > 0) {
return CommonResult.success(result);
} else {
return CommonResult.failed();
}
}
}
使用@RestController告诉spring这是一个控制器,Rest表示这个控制里面所有方法加上@ResponseBody也就是讲java对象转换成Json格式的数据返回。
@Api(tags = "BsnBusinessController", description = "商家管理")是swagger的一个接口描述
在方法上使用@ApiOperation("创建商家")是对具体方法(接口)的一个描述
所有方法都返回CommonResult对象,也就是前面leaf-common中定义的公共结果返回类。
dao、dto、service就没什么特别的了,这里就不说了。
后台的前端VUE使用的是一个名为vue-element-admin的开源项目大家可以看看https://github.com/PanJiaChen/vue-element-admin
五、leaf-business
他和总后台系统架构差不多,功能架构一模一样,只不过业务逻辑上他是商家进行商品录入、日常订单管理的一个后台,这里就不在进行赘述。
六、leaf-client
这个是用户端使用的接口服务,先从数据库连接池开始说起,使用druid配置了多数据源实现了读写分离
配置如下
spring:
datasource:
master:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://x.x.x.x:3306/leaf?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置初始值
initial-size: 5
min-idle: 10
max-active: 20
slave:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://x.x.x.x:3316/leaf?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: leaf
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置初始值
initial-size: 5
min-idle: 10
max-active: 20
流程概述如下:
1、添加数据源配置类,注入三个数据源master、slave和dataSourceRouter,在dataSourceRouter中加上@Primary提高优先级;
2、在dataSourceRouter中创建一个AbstractRoutingDataSource的实例DataSourceRouter,然后把master和slave设置到dataSourceRouter中,设置默认数据库为master,并且返回dataSourceRouter;
3、在进行数据库操作前都会调用dataSourceRouter类的方法determineCurrentLookupKey,在该方法中返回key就会选择相应的数据源。
4、创建一个注解类,类型分为master和slave,然后创建一个切面类,在service方法调用前查看注解类型,根据注解类型动态切换数据源。
具体代码如下:
创建DataSourceEnum枚举类
package com.funheng.leaf.client.datasource;
/**
* @author :leaf
* @date :Created in 2020/6/5 2:42 PM
* @description:
* @modified By:
*/
public enum DataSourceEnum {
MASTER("master"),
SLAVE("slave");
private String name;
private DataSourceEnum(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建自定义注解
package com.funheng.leaf.client.datasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author :leaf
* @date :Created in 2020/6/5 2:43 PM
* @description:
* @modified By:
* 数据源选择,自定义注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSourceAnnotation {
DataSourceEnum value() default DataSourceEnum.MASTER; // 默认主表master
}
创建切面类
package com.funheng.leaf.client.datasource;
/**
* @author :leaf
* @date :Created in 2020/6/5 2:45 PM
* @description:
* @modified By:
*/
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* AOP根据注解给上下文赋值
*/
@Aspect
@Order(1) // 数据源的切换要在数据库事务之前, 设置AOP执行顺序(需要在事务之前,否则事务只发生在默认库中, 数值越小等级越高)
@Component
public class DataSourceAspect {
private Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
// 切点, 注意这里是在service层
@Pointcut("execution(* com.funheng.leaf.client.service.*.*(..))")
public void aspect() {
}
@Before("aspect()")
private void before(JoinPoint point) {
log.info("-----------调用service----");
Object target = point.getTarget();
String method = point.getSignature().getName();
Class<?> classz = target.getClass();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
try {
Method m = classz.getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(DataSourceAnnotation.class)) {
DataSourceAnnotation data = m.getAnnotation(DataSourceAnnotation.class);
DataSourceContextHolder.putDataSource(data.value().getName());
log.info("-----------切换数据源, 上下文准备赋值-----:{}", data.value().getName());
log.info("-----------切换数据源, 数据源上下文实际赋值-----:{}", DataSourceContextHolder.getCurrentDataSource());
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 切面结束, 重置线程变量
@After("aspect()")
public void after(JoinPoint joinPoint) {
DataSourceContextHolder.removeCurrentDataSource();
log.info("重置数据源: Restore DataSource to [{}] in Method [{}]", DataSourceContextHolder.getCurrentDataSource(), joinPoint.getSignature());
}
}
创建动态数据源切换的上下文
package com.funheng.leaf.client.datasource;
/**
* @author :leaf
* @date :Created in 2020/6/5 2:58 PM
* @description:
* @modified By:
*/
public class DataSourceContextHolder {
private final static ThreadLocal<String> local = new ThreadLocal<>();
public static void putDataSource(String name) {
local.set(name);
}
public static String getCurrentDataSource() {
return local.get();
}
public static void removeCurrentDataSource() {
local.remove();
}
}
接下来创建数据源配置类
package com.funheng.leaf.client.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @author :leaf
* @date :Created in 2020/6/5 2:51 PM
* @description:数据源配置
* @modified By:
*/
@Configuration
@Slf4j
public class DataSourceConfig {
public final static String masterTransactionManager = "masterTransactionManager";
public final static String slaveTransactionManager = "slaveTransactionManager";
/***
* 注意这里用的 Druid 连接池
*/
@Bean(name = "dbMaster")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource dbMaster() {
log.info("master数据源");
return new DruidDataSource();
}
@Bean(name = "dbSlave")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource dbSlave() {
log.info("slave数据源");
return new DruidDataSource();
}
/***
* @Primary: 相同的bean中,优先使用用@Primary注解的bean.
* @Qualifier:: 这个注解则指定某个bean有没有资格进行注入。
*/
@Primary
@Bean(name = "dataSourceRouter") // 对应Bean: DataSourceRouter
public DataSource dataSourceRouter(@Qualifier("dbMaster") DataSource master, @Qualifier("dbSlave") DataSource slave) {
DataSourceRouter dataSourceRouter = new DataSourceRouter();
//配置多数据源
Map<Object, Object> map = new HashMap<>(5);
map.put(DataSourceEnum.MASTER.getName(), master); // key需要跟ThreadLocal中的值对应
map.put(DataSourceEnum.SLAVE.getName(), slave);
// master 作为默认数据源
dataSourceRouter.setDefaultTargetDataSource(master);
dataSourceRouter.setTargetDataSources(map);
return dataSourceRouter;
}
// 注入动态数据源 DataSourceTransactionManager 用于事务管理(事务回滚只针对同一个数据源)
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("dataSourceRouter") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
最后创建数据源实现类
package com.funheng.leaf.client.datasource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author :leaf
* @date :Created in 2020/6/5 2:58 PM
* @description:
* @modified By:
*/
/***
* AbstractRoutingDataSource抽象类, 实现AOP动态切换的关键
* 1.AbstractRoutingDataSource中determineTargetDataSource()方法中获取数据源
* Object lookupKey = determineCurrentLookupKey();
* DataSource dataSource = this.resolvedDataSources.get(lookupKey);
* 根据determineCurrentLookupKey()得到Datasource,并且此方法是抽象方法,应用可以实现
* 2.resolvedDataSources 的值根据 targetDataSources 所得 afterPropertiesSet()方法[该方法在@Bean所在方法执行完成后执行]中:
* Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()
* 3.然后在xml中使用<bean>或者代码中@Bean 设置 dataSource 的defaultTargetDataSource(默认数据源)和 targetDataSources(多数据源)
* 4.利用自定义注解,AOP拦截动态的设置ThreadLocal的值
* 5.在DAO层与数据库建立连接时会根据ThreadLocal的key得到数据源
*/
// 在访问数据库前会调用该类的 determineCurrentLookupKey() 方法获取数据库实例的 key
@Slf4j
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
log.info(" 当前数据源: " + DataSourceContextHolder.getCurrentDataSource());
return DataSourceContextHolder.getCurrentDataSource();
}
}
之后在service层的方法上添加@DataSourceAnnotation(value = DataSourceEnum.SLAVE)即可
package com.funheng.leaf.client.service.impl;
import com.funheng.leaf.client.dao.BusinessDao;
import com.funheng.leaf.client.datasource.DataSourceAnnotation;
import com.funheng.leaf.client.datasource.DataSourceEnum;
import com.funheng.leaf.client.domain.BusinessHomeQuery;
import com.funheng.leaf.client.domain.BusinessResultCommon;
import com.funheng.leaf.client.service.BsnBusinessService;
import com.funheng.leaf.mapper.BsnBusinessMapper;
import com.funheng.leaf.model.BsnBusiness;
import com.funheng.leaf.model.BsnBusinessExample;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author :leaf
* @date :Created in 2020/4/27 11:20 AM
* @description:
* @modified By:
*/
@Service
public class BsnBusinessServiceImpl implements BsnBusinessService {
@Autowired
BusinessDao businessDao;
@Override
@DataSourceAnnotation(value = DataSourceEnum.SLAVE)
public List<BusinessResultCommon> getList(Integer pageSize, Integer pageNum, BusinessHomeQuery businessHomeQuery) {
return businessDao.getBusinessByDistance(businessHomeQuery, (pageNum - 1) * pageSize, pageSize);
}
}
//TODO未完待续...