[日志、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
的读写分离策略,我们可以采用中间件的技术,如 MyCat
https://blog.csdn.net/ASYMUXUE/article/details/104964813等工具。这里我们介绍一种使用 SpringBoot
的AbstractRoutingDataSource
的接口,编码完成读写分离的方案。
该接口需要用户完善一个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>