使用spring实现的一个电商系统

该项目基于 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未完待续...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值