仅涉及后端,全部目录看顶部专栏,代码、文档、接口路径在:
【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客
全篇只介绍重点架构逻辑,具体编写看源代码就行,读起来也不复杂~
谨慎:源代码中有一些注释是错误的,有的注释意思完全相反,有的注释对不上号,我在阅读过程中就顺手更新了,并且在我不会的地方添加了新的注释,所以在读源代码过程中一定要谨慎啊!
目录
A2.日志处理(使用的slf4j+logback+AOP+注解)
A1.接口规范
接口规范按照 RESTful API接口规范,但是这个项目里面的接口规范还不太严谨,比如新增类型接口,有的用"/add",有的不用,并没有统一,之后自己开发的话还是要注意的(统一的接口规范对于前后端都是非常方便的)。
RESTful API接口规范可以看这篇:RESTful API接口规范
这儿就不重点讲了RESTful,然后还有一个需要规定的就是接口返值类型,一般项目都会自定义一个返回类型,方便前端接收处理。
//1.后端返回数据类,详见:cn.lili.common.vo.ResultMessage
//里面就包括接口规范的 result/data、code、message
@Data
public class ResultMessage<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 成功标志
*/
private boolean success;
/**
* 消息
*/
private String message;
/**
* 返回代码
*/
private Integer code;
/**
* 时间戳
*/
private long timestamp = System.currentTimeMillis();
/**
* 结果对象
*/
private T result;
}
//2.返回结果工具类,详见:cn.lili.common.utils.ResultUtil
//该类专门处理返回信息的,直接是用他提供的静态方法就可以方便的返回数据。
public class ResultUtil<T> {
/**
* 抽象类,存放结果
*/
private final ResultMessage<T> resultMessage;
/**
* 正常响应
*/
private static final Integer SUCCESS = 200;
/**
* 构造话方法,给响应结果默认值
*/
public ResultUtil() {
resultMessage = new ResultMessage<>();
resultMessage.setSuccess(true);
resultMessage.setMessage("success");
resultMessage.setCode(SUCCESS);
}
/**
* 抽象静态方法,返回结果集
* @param t 范型
* @param <T> 范型
* @return 消息
*/
public static <T> ResultMessage<T> data(T t) {
return new ResultUtil<T>().setData(t);
}
...
}
//使用范例
@Slf4j
@RestController
@Api(tags = "管理端,菜单管理接口")
@RequestMapping("/manager/permission/menu")
public class MenuManagerController {
@Autowired
private MenuService menuService;
@ApiOperation(value = "搜索菜单")
@GetMapping
public ResultMessage<List<Menu>> searchPermissionList(MenuSearchParams searchParams) {
return ResultUtil.data(menuService.searchList(searchParams));
}
...
}
这里是用了两个类,一个返回数据类,一个返回数据工具类,其实用一个工具类来完成这个逻辑,也是可以的。
A2.日志处理(使用的slf4j+logback+AOP+注解)
系统里面写的日志处理是使用的是log4j,但是代码里面使用的是 logback ,可能文档没更新吧。
日志的基本搭建就挺简单的,添加依赖,添加配置文件,就能够通过Logger使用了。
之后重点就是日志处理,如果某些日志处理业务繁琐,就会涉及到大量代码,并且操作也不方便,所以这样我们就不能直接在业务中使用Logger,我们需要进行处理。
常见的日志业务就是使用AOP+注解,将同样的日志业务抽象成切面,然后在需要实现该日志业务的方法上添加切点就行啦,例如系统日志就是很多业务都需要的日志处理
B1.日志基本搭建
springboot本身就自带了slf4j+logback依赖包,我们就不用依赖了。
然后在业务包里的resource的配置文件application.xml中设置配置信息,设置日志级别,设置日志存储路径等信息,例如manager-api模块中,
# /lilishop-master/manager-api/pom.xml
# logback日志
logging:
config: classpath:logback-spring.xml
# 输出级别
level:
cn.lili: info
# org.hibernate: debug
# org.springframework: debug
file:
# 指定路径
path: lili-logs
logback:
rollingpolicy:
# 最大保存天数
max-history: 7
# 每个文件最大大小
max-file-size: 5MB
然后添加logback配置文件,具体的我有时也记不住,可以见这篇文章:springboot使用logback日志框架超详细教程
//详见 :/lilishop-master/manager-api/src/main/resources/logback-spring.xml
然后启动程序时会根据配置的日志信息存储日志
B2.系统日志架构
springboot本身就自带了spring-boot-starter-aop依赖包,我们就不用依赖了。
系统本身就有日志业务,一般系统都有日志业务,那么就要结合系统日志业务来编写框架。
首先先确定产生的日志都包含操作名称、日志内容、操作人、操作时间等重要信息,并且某些日志内容还会拿取请求操作入参、出参中的内容,例如:日志内容=管理员登录请求:admin,admin就是获取的当前登录帐号的用户名。这儿的逻辑就靠 Spel 实现~
步骤
1.创建系统日志的注解类,里面参数包含操作名称、日志内容(操作内容是不会修改的、日志内容里面是有需要切换字段的);
2.创建系统日志切面类,这里是要有前置通知和后置通知都有,前置通知记录操作开始时间,然后在后置通知里面得到操作时间(操作开始时间-当前时间),并且通过 Spel 将日志内容的字段内容进行解析转化。日志数据准备完后,调用一个新线程来进行日志存储工作(开新线程不会影响当前线程操作)
注意:该系统将系统日志存储到了ES里面,并没有存到mysql数据库中,所以这里会使用到ES,我们在开发过程中可以暂时不写日志存储业务,在开发ES检索时添加~
//1.创建系统日志的注解类,里面参数包含操作名称、日志内容(操作内容是不会修改的、日志内容里面是有需要切换字段的);
//详见:cn.lili.modules.system.aspect.annotation.SystemLogPoint
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SystemLogPoint {
/**
* 日志名称
*
* @return
*/
String description() default "";
/**
* 自定义日志内容
*
* @return
*/
String customerLog() default "";
}
//使用例子,我们开发时可以直接加在管理员登录接口上测试
@Slf4j
@Service
public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser> implements AdminUserService {
@Override
@SystemLogPoint(description = "管理员登录", customerLog = "'管理员登录请求:'+#username")
public Token login(String username, String password) {
...
}
...
}
//2.创建系统日志切面类,这里是要有前置通知和后置通知都有,前置通知记录操作开始时间(时间要存到ThreadLocal里面,并且在后置通知里买呢要记得清除),然后在后置通知里面得到操作时间(操作开始时间-当前时间),并且通过 Spel 将日志内容的字段内容进行解析转化。日志数据准备完后,调用一个新线程来进行日志存储工作(开新线程不会影响当前线程操作)
//系统日志切面类,详见:cn.lili.modules.system.aspect.interceptor.SystemLogAspect
//Spel工具类,详见:cn.lili.common.utils.SpelUtil
//保存系统日志线程类,详见:cn.lili.modules.system.aspect.interceptor.SystemLogAspect.SaveSystemLogThread
//系统日志存储业务,不是一个类哦,是涉及到service和ElasticsearchRepository,我们可以先把service完成,ES留到开发ES时再写也可以,详见:cn.lili.modules.permission.serviceimpl.SystemLogServiceImpl、cn.lili.modules.permission.repository.SystemLogRepository
//切面类
@Aspect
@Component
@Slf4j
public class SystemLogAspect {
/**
* 启动线程异步记录日志
*/
private static final ThreadLocal<Date> BEGIN_TIME_THREAD_LOCAL = new NamedThreadLocal<>("SYSTEM-LOG");
@Autowired
private SystemLogService systemLogService;
@Autowired
private HttpServletRequest request;
@Autowired
private IpHelper ipHelper;
/**
* Controller层切点,注解方式
*/
@Pointcut("@annotation(cn.lili.modules.system.aspect.annotation.SystemLogPoint)")
public void controllerAspect() {
}
/**
* 前置通知 (在方法执行之前返回)用于拦截Controller层记录用户的操作的开始时间
*/
@Before("controllerAspect()")
public void doBefore() {
BEGIN_TIME_THREAD_LOCAL.set(new Date());
}
/**
* 后置通知(在方法执行之后并返回数据) 用于拦截Controller层无异常的操作
* JoinPoint 连接点,可以获取被代理方法的各种信息,如方法参数,方法所在类的class对象
*
* @param joinPoint 切点
*/
@AfterReturning(returning = "rvt", pointcut = "controllerAspect()")
public void after(JoinPoint joinPoint, Object rvt) {
try {
//获取注解中对方法的描述信息,并将内容通过 SPEL 进行解析转化
Map map = this.spelFormat(joinPoint, rvt);
String description = map.get("description").toString();
String customerLog = map.get("customerLog").toString();
Map<String, String[]> logParams = request.getParameterMap();
AuthUser authUser = UserContext.getCurrentUser();
SystemLogVO systemLogVO = new SystemLogVO();
if (authUser == null) {
// -2 代表游客
systemLogVO.setStoreId(-2L);
//请求用户
systemLogVO.setUsername("游客");
} else {
//如果是商家/买家则记录商家/买家id,否则记录-1,代表平台id
systemLogVO.setStoreId(authUser.getRole().equals(UserEnums.STORE) ? Long.parseLong(authUser.getStoreId()) : -1);
//请求用户
systemLogVO.setUsername(authUser.getUsername());
}
//日志标题
systemLogVO.setName(description);
//日志请求url
systemLogVO.setRequestUrl(request.getRequestURI());
//请求方式
systemLogVO.setRequestType(request.getMethod());
//请求参数
systemLogVO.setMapToParams(logParams);
//响应参数 此处数据太大了,所以先注释掉
// systemLogVO.setResponseBody(JSONUtil.toJsonStr(rvt));
//请求IP
systemLogVO.setIp(IpUtils.getIpAddress(request));
//IP地址
systemLogVO.setIpInfo(ipHelper.getIpCity(request));
//写入自定义日志内容
systemLogVO.setCustomerLog(customerLog);
//请求开始时间
long beginTime = BEGIN_TIME_THREAD_LOCAL.get().getTime();
long endTime = System.currentTimeMillis();
//请求耗时
Long usedTime = endTime - beginTime;
systemLogVO.setCostTime(usedTime.intValue());
//调用线程保存
ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(systemLogVO, systemLogService));
} catch (Exception e) {
log.error("系统日志保存异常", e);
}finally {
BEGIN_TIME_THREAD_LOCAL.remove();
}
}
/**
* 保存日志的线程类
*/
private static class SaveSystemLogThread implements Runnable {
@Autowired
private SystemLogVO systemLogVO;
@Autowired
private SystemLogService systemLogService;
public SaveSystemLogThread(SystemLogVO systemLogVO, SystemLogService systemLogService) {
this.systemLogVO = systemLogVO;
this.systemLogService = systemLogService;
}
@Override
public void run() {
try {
systemLogService.saveLog(systemLogVO);
} catch (Exception e) {
log.error("系统日志保存异常,内容{}:", systemLogVO, e);
}
}
}
/**
* 获取注解中对方法的描述信息,并将内容通过 SPEL 进行解析转化
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
private static Map<String, String> spelFormat(JoinPoint joinPoint, Object rvt) {
Map<String, String> result = new HashMap<>(2);
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
SystemLogPoint systemLogPoint = signature.getMethod().getAnnotation(SystemLogPoint.class);
String description = systemLogPoint.description();
//调用 sqel 工具,解析转化日志内容
String customerLog = SpelUtil.compileParams(joinPoint, rvt, systemLogPoint.customerLog());
result.put("description", description);
result.put("customerLog", customerLog);
return result;
}
}
我们现在进行管理员登录操作时,就会在执行接口之后进去切面,执行系统日志操作~~~但要记得现在还没有存储日志哦
B3.日志整合logstash(待更新)
logstash待学习 ~