项目实施-03(后端组件整合)

[日志、Rest测试、读写分离]

概述

本篇进行对项目实施-02(后端开发)的后端模块https://blog.csdn.net/ASYMUXUE/article/details/104920206进行组件扩展。因为,此篇章将做成通用性极强的记录,所以本篇将不定期更新。

日志系统的整合

引入logback.xml文件

标签说明

%m 输出代码中指定的消息
  %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
  %r 输出自应用启动到输出该log信息耗费的毫秒数
  %c 输出所属的类目,通常就是所在类的全名
  %t 输出产生该日志事件的线程名
  %n 输出一个回车换行符,Windows平台为“/r/n”,Unix平台为“/n”
  %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss , SSS},输出类似:2002年10月18日 22 : 10 : 28 , 921
  %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java: 10 )

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
	<!--向控制台输出-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender" >
        <encoder>
            <pattern>%p %c#%M %d{yyyy-MM-dd HH:mm:ss} %m%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
	<!--向日志文件输出-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
           <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
              <fileNamePattern>logs/userLoginFile-%d{yyyyMMdd}.log</fileNamePattern>
              <maxHistory>30</maxHistory>
           </rollingPolicy>
           <encoder>
              <pattern>%p %c#%M %d{yyyy-MM-dd HH:mm:ss} %m%n</pattern>
        <charset>UTF-8</charset>
           </encoder>
    </appender>

    <!-- 控制台输出日志级别 -->
    <root level="ERROR">
        <appender-ref ref="STDOUT" />
    </root>

    <logger name="org.springframework.jdbc" level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>

    <logger name="com.baizhi.dao" level="TRACE" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>
    <logger name="com.baizhi.controller" level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>
    <logger name="com.baizhi.cache" level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>
</configuration>

Spring REST 测试

Spring对REST的支持是构建在Spring MVC之上的

①注册RestTemple组件
package com.baizhi;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.web.client.RestTemplate;

import java.net.UnknownHostException;

@SpringBootApplication
@MapperScan("com.baizhi.dao")
public class UsermodelApplication {

    public static void main(String[] args) {
        SpringApplication.run(UsermodelApplication.class, args);
    }

    //注册一个Rest组件 SpringMVC中自带的一个Rest客户端工具,可以无缝和SpringBoot集成
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}
②使用Rest测试

package com.baizhi.controller;

import com.baizhi.UsermodelApplication;
import com.baizhi.entities.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.omg.CORBA.OBJECT_NOT_EXIST;
import org.omg.CORBA.OBJ_ADAPTER;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.HashMap;

import static org.junit.Assert.*; //引入断言
@RunWith(SpringRunner.class)
@SpringBootTest(classes = UsermodelApplication.class)
public class FormControllerTest {

    @Autowired //自动注入rest模板类
    private RestTemplate restTemplate;
    //定义一个访问地址抬头
    private String prefix="http://localhost:9090/user/formUserManager";

    /*
    @PostMapping(value = "/registerUser")
     public User registerUser(User user,
                              @RequestParam(value = "multipartFile",required = false) MultipartFile multipartFile)
                              */
    //测试注册
    @Test
    public void testRegistUser(){
        //定义完整的访问路径
        String url = prefix+"/registerUser";
        //构建表单参数,创建一个MultiValueMap 对象
        MultiValueMap<String,Object> formData = new LinkedMultiValueMap<String, Object>();
        formData.add("name","测试数据6");
        formData.add("password","000000");
        formData.add("sex","true");
        formData.add("birthDay","2019-07-01");
        formData.add("email","000000@163.com");
        //上传文件信息
        //获取文件流
        FileSystemResource fileSystemResource = new FileSystemResource("C:\\Users\\15659\\OneDrive\\图片\\本机照片\\c044b7e72b514270ab54a6224c1cb251.jpg");
        formData.add("multipartFile",fileSystemResource);

        //发送数据
        User user = restTemplate.postForObject(url,formData, User.class);//1.请求地址 2.请求数据 3.响应类型
        //下断言
        assertNotNull("用户",user);
        System.out.println(user);
    }

      /* ---------------------------------------------------------------

     纯 restful 风格的测试 使用了对象传参
    private String urlPrefix="http://localhost:8888/restUserManager";


         @PostMapping(value = "/registerUser")
        public User registerUser(@RequestPart(value = "user") User user,
                                 @RequestParam(value = "multipartFile",required = false) MultipartFile multipartFile) throws IOException {

    @Test
    public void testRegisterUser(){
        String url=urlPrefix+"/registerUser";
        //模拟表单数据
        MultiValueMap<String,Object> formData=new LinkedMultiValueMap<String,Object>();
        User user =new User("233",true,"123456",new Date(),"aa.png","1152926811@qq.com");
        formData.add("user",user);
        //模拟文件上传
        FileSystemResource fileSystemResource=new FileSystemResource("/Users/admin/Desktop/head.png");
        formData.add("multipartFile",fileSystemResource);
        User registerUser = restTemplate.postForObject(url, formData, User.class);
        assertNotEquals("用户ID",registerUser.getId());
    }
  ---------------------------------------------------------------  */

  /*    //删除
    @DeleteMapping(value = "/deleteUserByIds")
    public void delteUserByIds(@RequestParam(value = "ids") Integer[] ids)
*/
     @Test
     public void testDelteUserByIds(){
        //定义链接地址
        String url = prefix+"/deleteUserByIds?ids={id}";
        //创建一个传参对象
         HashMap<String, Object> map = new HashMap<>();
         //为map赋值
         map.put("id","6,7,12");
         //执行
         restTemplate.delete(url,map);

     }

     /*
     *  //按照属性分页模糊查询
    @GetMapping(value = "/queryUserByPage")
    public List<User> queryUserByPage(@RequestParam(value = "page",defaultValue = "1") Integer pageNow,
                                      @RequestParam(value = "rows",defaultValue = "10") Integer pageSize,
                                      @RequestParam(value = "column",required = false) String column,
                                      @RequestParam(value = "value",required = false) String value)
     * */
     @Test
    public void testQueryUserByPage(){
         //定义地址
         String url = prefix+"/queryUserByPage?page={p}&rows={r}&column={c}&value={v}";
         //构建传参集合
         HashMap<String, Object> map = new HashMap<>();
         //传递参数
         map.put("p",3);
         map.put("r",2);
         map.put("c","name");
         map.put("v","数据");
         //开始执行
         User[] users = restTemplate.getForObject(url, User[].class, map);
         //下断言
         assertNotNull("查询到了这些用户",users);
         for (User user : users) {
             System.out.println("user = " + user);
         }
     }

     /*
     修改
    @PutMapping(value = "/updateUser")
    public void updateUser(User user,
                           @RequestParam(value = "multipartFile",required = false) MultipartFile multipartFile)
     * */

    @Test
    public void testUpdateUser(){
        //构建地址
        String url = prefix+"/updateUser";
        //创建传参的集合对象
        LinkedMultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>();
        //传递参数
        multiValueMap.add("id",13);
        multiValueMap.add("name","修改数据2");
        multiValueMap.add("sex",true);
        multiValueMap.add("password",000000);
        multiValueMap.add("birthDay","2013-01-01");
        multiValueMap.add("email","2013@qq.com");
        //获取文件的输入流
        FileSystemResource fileSystemResource = new FileSystemResource("C:\\Users\\15659\\OneDrive\\图片\\本机照片\\c044b7e72b514270ab54a6224c1cb251.jpg");
        multiValueMap.add("multipartFile",fileSystemResource);
        //执行
        restTemplate.put(url,multiValueMap);

    }

}

MySQL读写分离

实现MySQL的读写分离策略,我们可以采用中间件的技术,如 MyCathttps://blog.csdn.net/ASYMUXUE/article/details/104964813等工具。这里我们介绍一种使用 SpringBootAbstractRoutingDataSource的接口,编码完成读写分离的方案。
该接口需要用户完善一个determineCurrentLookupKey抽象法,系统会根据这个抽象返回值决定使用系统中定义的数据源。

①配置自定义数据源信息

在yml文件中配置自定义数据源。

  #配置数据源(读写分离数据源)
  datasource:
    #配置自定义数据源1
    master:
      username: root
      password: 0
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://10.10.0.151:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
    #配置自定义数据源2
    slave1:
      username: root
      password: 0
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://10.10.0.152:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
    #配置自定义数据源3
    slave2:
      username: root
      password: 0
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://10.10.0.152:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
②定义中间类

定义一个枚举类型存放读与写的两种状态值。

package com.baizhi.dataSource;

/**
 * 定义操作类型
 */
public enum OperType {
    WRIRTE,READ;
}

定义一个用于持有当前事务操作状态的类

package com.baizhi.dataSource;

/**
 * 通过线程本地变量传递操作类型
 */
public class OperTypeContextHolder  {
    private static final ThreadLocal<OperType> OPER_TYPE_THREAD_LOCAL=new ThreadLocal<>();

    public static void setOperType(OperType operType){
        OPER_TYPE_THREAD_LOCAL.set(operType);
    }
    public static OperType getOperType(){
        return OPER_TYPE_THREAD_LOCAL.get();
    }
    public static void clear(){
        OPER_TYPE_THREAD_LOCAL.remove();
    }
}
③实现AbstractRoutingDataSource接口
package com.baizhi.dataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**创建一个实现 AbstractRoutingDataSource 的类,在此类中,可以获取当前选择的数据源*/
public class DataSourceProxy extends AbstractRoutingDataSource {
    private static final Logger logger= LoggerFactory.getLogger(DataSourceProxy.class);

    /**定义key,系统将根据此处的返回值,调用被管理的数据代理对象中的相应key的数据源*/
    private  String masterDBKey="master";
    private List<String> slaveDBKeys= Arrays.asList("slave-01","slave-02");

    //定义一个原子整数
    private static final AtomicInteger round=new AtomicInteger(0);
    /**
     * 需要在该方法中,判断当前用户的操作是读操作还是写操作
     * 以后系统会根据determineCurrentLookupKey方法的返回值作为key从targetDataSources查找相应的实际数据源。如果找不到则使用defaultTargetDataSource指定的数据源。
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String dbKey=null;
        /**定义一个持有当前业务操作状态的类,从此类获取读/写操作,并以此判定返回数据源的key*/
        OperType operType = OperTypeContextHolder.getOperType();
        //如果时刻写操作,返回
        if(operType.equals(OperType.WRIRTE)){
            dbKey=masterDBKey;
        }else{
            //轮询返回  0 1 2 3 4 5 6
            int value = round.getAndIncrement();
            if(value < 0){
                round.set(0);
            }
            Integer index=round.get()%slaveDBKeys.size();

            dbKey=slaveDBKeys.get(index);
        }
        logger.debug("当前的DBkey:"+dbKey);
        return dbKey;
    }
}


④提交所有数据源给工厂

定义一个代理类,它将持有所有自定义的数据源工厂将根据 ·AbstractRoutingDataSource·接口实现类的返回值,匹配对应key的数据源

package com.baizhi.dataSource;


import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

//创建一个自定义的数据源类
@Configuration //指出该类是 Bean 配置的信息源
public class UserDefineDatasourceConfig {
    //将数据源1交给工厂管理
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    //将数据源2交给工厂管理
    @Bean
    @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }
    //将数据源3交给工厂管理
    @Bean
    @ConfigurationProperties("spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }

    /**定义代理数据源 当有多个同一类型的Bean时,可以用@Qualifier("name")来指定。*/
    @Bean
    public DataSource proxyDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                      @Qualifier("slave1DataSource") DataSource slave1DataSource,
                                      @Qualifier("slave2DataSource") DataSource slave2DataSource){
        /**创建一个实现 AbstractRoutingDataSource 的类,在此类中的determineCurrentLookupKey返回值可以获取当前选择的数据源的key*/
        DataSourceProxy proxy = new DataSourceProxy();
        //为此实现类赋予 默认的数数据源,与目标数据源
        proxy.setDefaultTargetDataSource(masterDataSource);//设置默认数据源
        //创建一个map用于存储目标数据源
        Map<Object,Object> mappedDataSource=new HashMap<>();
        mappedDataSource.put("master",masterDataSource);
        mappedDataSource.put("slave-01",slave1DataSource);
        mappedDataSource.put("slave-02",slave2DataSource);

        proxy.setTargetDataSources(mappedDataSource); //注册所有数据源
        //总终此代理拿到所有数据源
        return proxy;
    }

    /**
     * 当自定义数据源,用户必须覆盖SqlSessionFactory创建
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("proxyDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.baizhi.entities");
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:com/baizhi/mapper/*.xml"));

        return  sqlSessionFactoryBean.getObject();
    }

    /**
     * 当自定义数据源,用户必须覆盖SqlSessionTemplate,开启BATCH处理模式
     * @param sqlSessionFactory
     * @return
     */
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);

    }

    /***
     * 当自定义数据源,用户必须注入,否则事务控制不生效
     * @param dataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager platformTransactionManager(@Qualifier("proxyDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

⑤使用AOP编程,获取事务操作状态

定义一个自定义注解

package com.baizhi.dataSource;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 标记的业务方法是否是读操作
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface SlaveDB {
}

在业务方法的所有读操作上添加此注解

 //查
    @SlaveDB
    @Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
    @Override//按照ID查一个
    public User queryUserById(Integer id) {
        User user = iUserDAO.queryUserById(id);
        return user;
    }

  • AOP编程获取事务操作属性,判断读写类型
package com.baizhi.dataSource;

import com.baizhi.dataSource.OperType;
import com.baizhi.dataSource.OperTypeContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
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;

@Aspect
@Order(0) //控制切面顺序,保证在事务切面之前运行切面
@Component
public class ServiceMethodAOP {
    private static final Logger logger= LoggerFactory.getLogger(ServiceMethodAOP.class);

    @Around("execution(* com.baizhi.service..*.*(..))")
    public Object methodInterceptor(ProceedingJoinPoint pjp){
        Object result = null;
        try {
            //获取当前的方法信息
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Method method = methodSignature.getMethod();
            //判断方法上是否存在注解@SlaveDB
            boolean present = method.isAnnotationPresent(SlaveDB.class);

            OperType operType=null;
            if(!present){
                operType=OperType.WRIRTE;
            }else{
                operType=OperType.READ;
            }
            OperTypeContextHolder.setOperType(operType);
            logger.debug("当前操作:"+operType);
            result = pjp.proceed();
            //清除线程变量
            OperTypeContextHolder.clear();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return result;
    }
}

开启此包的日志

	 <logger name="com.baizhi.dataSource" level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT" />
    </logger>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值