理解AOP
AOP,面向切面编程。
- 举一个不是很恰当的例子:在一个工厂的车间中,有很多工人在工作,为了保证车间的生产效率,
管理人员都会给车间装上摄像头,通过摄像头来反映车间内部工人的工作情况。摄像头就是从侧面监控的,就相当于我们的切面,我们通过摄像头就不会打扰到车间工人的工作,我们不用走进车间去问每个工人做了多少事,做了哪些事,一切都交给了摄像头来记录。车间就相当于我们的任务主体,比喻说我们程序运行时,用户做的CRUD。
所以,AOP采取的就是横向的抽取机制,取代了传统的纵向继承体系重复性
AOP,应用场景
- 性能监视,
- 事务管理,
- 安全检查,
- 缓存,
- 扩展日志功能
等等
AOP,操作术语 - JoinPoint连接点:类里面可以被增强的方法
- Pointcut切入点:实际被增强的方法,也就是新增的方法
- Advice通知/增强:增强的逻辑,类型:前置通知、后置通知、异常通知、最终通知、环绕通知
- Aspect切面:把增强应用到具体方法上,过程称为切面
使用表达式配置切入点
- 切入点:实际被增强的方法
- 常用表达式——execution表达式
语法: execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)
例如: execution(* com.baobaotao.Waiter.*(…))匹配Waiter接口的所有方法,它匹配NaiveWaiter和
NaughtyWaiter类的greetTo()和serveTo()方法。第一个代表返回任意类型,
com.baobaotao.Waiter.代表Waiter接口中的所有方法;
AOP实例——通过自定义注解,实现日志功能
本实例借鉴了Eladmin开源项目中的日志功能
- 首先看下用日志存储的数据库表
requestip该字段会获取用户ip,但是如果你本地启动项目localhost 则表现为0:0:0:0:0:0:0:1。如果是上线外网用户访问,则会通过ip地址得到该用户的所在地xx省xx市 - 要是现实该功能需要用到如下依赖
<!--获取浏览器信息 必须依赖-->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
<!--获取用户ip 必须依赖-->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
<!-- 阿里巴巴json处理工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- hutool工具包包含有很多工具类 文件上传 下载导入导出等等非常丰富 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
<!-- 工具依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- lombok 可选依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 阿里连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 以下是常用依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- 安全框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring AOP 必须依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 配置文件
server.port=8080
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql:///customer?serverTimezone=GMT
- 通过ip查询地址需要用到ip2region库
放在resource下面即可 - 会使用到的工具类
在utils包下
FileUtils —— 将nputStream 转 File对象 该类继承了hutool工具类(上文中依赖中的工具类)
该类主要是为通过ip查询地址而服务
RequestUtils —— 获取请求信息,通过request请求获取用户的ip
SecurityUtils —— 安全框架获取用户名和用户详细信息
StringUtils —— 有两个方法获取ip 和 根据ip获取详细地址 - 编写安全配置类
简要配置了下,主要是学习AOP,密码是123
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("test").roles("user").password("$2a$10$V0VWQ4I0Hr/Z2Be2veQh3emu2fbl9eH80RK7wuGEkR7BeDORutiSa")
.and().withUser("zddu").roles("admin").password("$2a$10$V0VWQ4I0Hr/Z2Be2veQh3emu2fbl9eH80RK7wuGEkR7BeDORutiSa");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
.formLogin() //表单登录配置
.loginProcessingUrl("/login")//表单登陆提交数据的路径
.and()
.csrf().disable();
}
}
- 创建自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)// 注解会在class字节码文件中存在,在运行时可以通过反射获取到
public @interface Log {
String value() default "";
}
- 编写切面类
/**
* 切面
*/
@Component
@Aspect
@Slf4j
public class LogAspect {
@Autowired
private LogService logService;
@Pointcut("@annotation(com.cidp.aoplog.aop.Log)")
public void logPointcut(){
// 该方法无方法体,主要为了让同类中其他方法使用此切入点
}
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result;
result = joinPoint.proceed(); //获取目标方法的返回值
com.cidp.aoplog.model.Log log = new com.cidp.aoplog.model.Log("INFO");
HttpServletRequest req = RequestUtils.getHttpServletRequest();
logService.save(getUsername(), StringUtils.getBrowser(req),StringUtils.getIp(req),joinPoint,log);
return result;
}
public String getUsername() {
try {
return SecurityUtils.getUsername();
}catch (Exception e){
return "";
}
}
}
主要使用到了环绕通知。
- Log实体类
- 这里用到lombok 省去了自己生成getter和setter等方法
@Data
public class Log {
private Integer id;
/**请求用户*/
private String username;
private String description;
private String method;
private String logType;
private String requestIp;
private String address;
private String browser;
private String createDate;
public Log(String logType) {
this.logType = logType;
}
}
- logMapper
我这里用到了xml,大家可以根据自己的来
public interface logMapper {
void InsertLog(Log log);
}
- logService
public class LogService {
@Autowired
logMapper logMapper;
@Async //该方法为异步方法
public void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, Log log) {
SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//通过反射机制 拿到注解中的操作名description
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
com.cidp.aoplog.aop.Log description = method.getAnnotation(com.cidp.aoplog.aop.Log.class);
//方法路径 例如:com.cidp.aoplog.controller.getUserName() 方法路径
String methodName = joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()";
if (log !=null){
log.setDescription(description.value());
log.setRequestIp(ip);
}
log.setAddress(StringUtils.getCityInfo(log.getRequestIp()));
log.setMethod(methodName);
log.setUsername(username);
log.setBrowser(browser);
log.setCreateDate(dateFormat.format(new Date()));
logMapper.InsertLog(log);
}
}
- 测试接口
@RestController
public class TestController {
@Log("查询测试")
@GetMapping("/hello")
public String hello(){
return "aop log1";
}
@Log("删除测试")
@GetMapping("/test")
public String hello2(){
return "aop log2";
}
}
- 测试结果
- 项目案例下载地址
路漫漫其修远兮