Aop(面向切面编程):
借用一图,我自己浅显的理解为将许多简单,但是有用到的方法抽取出来,降低代码耦合度,更关注与核心功能。例如,对于常用的日志功能,我们可以不用在每个模块中重复的日志代码,而是使用动态代理的原理:使用一个代理将对象包裹起来,之后每次对这个对象的调用都要通过这个代理。
1、AOP概念及注解
切面(Aspect):一个关注点的模块化。以注解@Aspect的形式放在类上方,声明一个切面。
(Pointcut和Advice的结合)
连接点(Joinpoint):在程序执行过程中某个特定的点,比如某个方法调用或者处理异常时候都可以是连接点。
切点(Pointcut):筛选出的连接点,一个类中的所有方法都是连接点,但是又不全需要,会筛选出某些作为连接点作为切点。如果说通知定义了切面的动作或执行时机的话,切点则定义了执行的低点。切点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
两种方式:一种是注解,一种是切点表达式execution(…)
引入(Introduction):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让他们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去。
目标对象(Target Object):被一个或者多个切面所通知的对象。也被称作被(adviced)对象。既然Spring AOP是用过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
织入(Weaving):把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
通知(Advice):通知增强,需要完成的工作叫做通知,就是业务逻辑中需要的比如事务、日志等先定义好,然后再需要的地方使用。
@Aspect:将当前类作为一个切面类
@Component:Spring注解
@Pointcut:切点
注解的方式,定义我们的匹配规则,括号中的是我们自定义的注解的路径
@Around:属于环绕增强,能控制切点执行前后,使用该注解后,当程序抛出异常会影响@AfterThrowing注解(环绕通知,可以同时在所拦截方法的前后,执行一段逻辑)。
@Before:前置通知,在切点方法之前执行(在所拦截方法执行之前执行一段逻辑)。
通过JoinPoint参数获取目标方法的方法名、修饰符等信息。
@After:后置通知,在切点方法之后执行。
@AfterReturning:返回通知,切点方法返回后执行。在该方法中可以获取目标方法返回值。returning参数是指返回值的变量名,对应方法的参数。
@AfterThrowing:异常通知,切点方法抛异常执行。
2、日志功能
2.1、mybatis-plus 代码生成
(1)数据库建表:
CREATE TABLE `sys_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`user_action` varchar(255) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
(2)导入相关依赖(springboot):
<!--mybaits-plus依赖->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!--代码生成器--!>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<!--模板引擎--!>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.29</version>
</dependency>
(3)aop依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
(3)代码生成类
官网链接 我自己的代码:
此处要注意,假如要生成id,一定要删除
数据源配置注意路径和登陆的账号密码
package com.example.wxs.aicode;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @author cmy 2020.03.04
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("cmy");
gc.setOpen(false);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/wxdatas?serverTimezone=UTC&characterEncoding=utf-8&serverTimezone=Asia/Shanghai");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("模块名"));
pc.setParent("com.example.wxs");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 写于父类中的公共字段
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
自动生成的目录如下:
注意假如你的项目有多个mapper包,请在启动类上一定要扫描到,
(4)自定义注解
启动类同级的包下新建
package com.example.wxs.configs;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//说明注解所修饰的范围,这边是用在方法
@Target(ElementType.METHOD)
//注解的注解,被它修饰的注解的生命周期,这边是指被修饰的注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
(5)服务层接口和实现类
package com.example.wxs.aoplog.service;
import com.example.wxs.aoplog.entity.SysLog;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author cmy
* @since 2020-03-04
*/
public interface ISysLogService extends IService<SysLog> {
/**
* 插入日志
* @param entity
* @return
*/
int insertLog(SysLog entity);
}
package com.example.wxs.aoplog.service.impl;
import com.example.wxs.aoplog.entity.SysLog;
import com.example.wxs.aoplog.mapper.SysLogMapper;
import com.example.wxs.aoplog.service.ISysLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author cmy
* @since 2020-03-04
*/
@Service
public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> implements ISysLogService {
@Autowired
private SysLogMapper sysLogMapper;
@Override
public int insertLog(SysLog entity) {
// TODO Auto-generated method stub
return sysLogMapper.insert(entity);
}
}
2.2、切面类
启动类同级包下新建
package com.example.wxs.configs;
import com.example.wxs.aoplog.entity.SysLog;
import com.example.wxs.aoplog.service.impl.SysLogServiceImpl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
/**
* @author cmy 2020.03.04
*/
@Aspect
@Component
public class LogAsPect {
private final static Logger log=org.slf4j.LoggerFactory.getLogger(LogAsPect.class);
@Autowired
private SysLogServiceImpl sysLogService;
@Pointcut("@annotation(com.example.wxs.configs.Log)")
public void pointcut() {}
@Around("pointcut()")
/*Proceedingjoinpoint 继承了JoinPoint ,在JoinPoint 的基础上暴露了proceed()方法,暴露
*出这个方法,就能支持 aop:around 这种切面,也就是说Proceedingjoinpoint 才支持环绕通知
*/
public Object around(ProceedingJoinPoint point){
Object result=null;
long beginTime = System.currentTimeMillis();
try{
log.info("我在目标方法之前执行!");
//启动目标方法执行
result=point.proceed();
long endTime = System.currentTimeMillis();
insertLog(point,endTime-beginTime);
}catch (Throwable e){
}
return result;
}
//插入到之前的日志表中
private void insertLog(ProceedingJoinPoint point,long time){
//获取方法(MethodSignature主要实现的是返回值类,方法名和形式参数)
MethodSignature signature=(MethodSignature)point.getSignature();
Method method=signature.getMethod();
SysLog sys_log=new SysLog();
//如果存在这样的注释,则返回指定类型的元素的注释
//例如,@Log("插入人员"),userAction ="插入人员"
Log userAction =method.getAnnotation(Log.class);
if(userAction!=null){
sys_log.setUserAction(userAction.value());
}
//获取被代理的对象,通过反射获取类名
String className=point.getTarget().getClass().getName();
String methodName=signature.getName();
//获取方法的参数
String args= Arrays.toString(point.getArgs());
int userid=1;
sys_log.setUserId(userid);
sys_log.setCreateTime(new java.sql.Timestamp(new Date().getTime()));
log.info("登陆人:{},类名:{},方法名:{},参数:{},执行时间:{}",userid, className, methodName, args, time);
sysLogService.insertLog(sys_log);
}
@Pointcut("execution(public * com.example.wxs.controller..*.*(..))")
public void pointcutController(){
}
@Before("pointcutController()")
public void around2(JoinPoint point){
//获取目标方法路径/名和参数
String methodNam = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
String params = Arrays.toString(point.getArgs());
log.info("get in {} params :{}",methodNam,params);
}
}
@“execution(* ….(…))”
表示匹配所有方法
@execution(public * com.example.wxs.controller….(…))
表示匹配com.example.wxs.controller所有公有方法
@execution(* com.example.wxs….(…))
表示匹配com.example.wxs包及其子包下的所有方法
测试
①、用户执行插入一条记录的功能,日志表插入一条数据
在服务层的实现类上加注解@Log,对应之前新建的自定义注解Log。
切面类中的代码:
实现类中的代码:
效果:
②、用户执行插入一条记录的功能,开发者控制台输出
com.example.wxs.controller所有公有方法执行之前都会打印。
效果: