AOP(一)——springAOP
AOP面向切面编程。至于理论网上有很多。个人理解为对待执行的方法进行拦截,拦截后就可以为所欲为,想先执行些前置逻辑,或者待拦截方法执行后执行一些后置逻辑等。
正如夹心饼干,一分为二,中间可以加草莓酱,蓝莓酱,奶油酱,等等。
废话不多说,先来个代码实例。
一、初试牛刀
有这么一个ProductService,需要执行插入操作,删除操作等。但是现在老板突然想执行这些操作前校验一下权限,OK,你一个个方法做修改,最后改成如下格式。
@Service
public class ProductService {
@Autowired
AuthService authService;
public void insert(Product product) {
authService.checkAccess();//权限校验
System.out.println("Insert product");
}
public void delete(long id) {
System.out.println("Delete product id="+id);
}
}
AuthService中有一个checkAccess()方法就是用来校验权限的,上述改完后你心满意足,如果还有其他上百个方法同时需要权限校验呢?
还房贷和车贷的压力让你不敢甩手不干,难道你要一个个修改方法么?这是你需要AOP,不要998,只要一些jar包。
下面是使用AOP的方法
首先编写一个注解,用在方法上表示这是admin用户才有权限操作的方法。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AdminOnly {
}
然后将ProductService的insert加上注解,那么就可以进行权限校验了?别傻了,还需要你写切面类去拦截,不然系统怎么知道你这个@AdminOnly用来干什么的,人工智能没有这么先进。
@AdminOnly
public void insert(Product product) {
System.out.println("Insert product");
}
下面写一个SecurityAspect.java切面类,用来处理@AdminOnly注解
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.lw.service.AuthService;
@Aspect//表示这是一个切面类
@Component//表示这个切面类交给spring容器管理
public class SecurityAspect {
@Autowired
AuthService authService;
/**
* 拦截注解为@AdminOnly的方法
*/
@Pointcut("@annotation(AdminOnly)")
public void adminOnly() {
}
/**
* 在执行adminOnly方法之前需要的操作
*/
@Before("adminOnly()")
public void check() {
authService.checkAccess();
}
}
OK,这样就能同下面代码一样insert的时候进行权限校验。
@Autowired
AuthService authService;
public void insert(Product product) {
authService.checkAccess();
System.out.println("Insert product");
}
这样就结束了么?作为一个英俊潇洒,风流倜傥,勤恳认真(……)的好程序猿当然要写个测试类。
上测试类SpringAopTest.java,这个测试类可牛逼了使用@SpringBootTest,那么自然能想到使用的是SpringBoot,得需要写个SpringBoot的启动类。
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAopTest {
@Autowired
ProductService productService;
/**
* 匿名插入权限校验
*/
@Test(expected=Exception.class)
public void annoInsertTest() {
CurrentUserHolder.set("king");
productService.insert(null);
}
@Test
public void adminInsertTest() {
CurrentUserHolder.set("admin");
productService.insert(null);
}
}
SpringBoot的启动类ApplicationContext.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApplicationContext {
public static void main(String[] args) {
SpringApplication.run(ApplicationContext.class, null);
}
}
至于springboot还需要application.yml配置文件就不列出来了,反正也是个空文件。
至此结束。
What?你他丫逗我呢?我不还是一个个方法加上@AdminOnly注解,我不还是累个半死,还想着使用AOP能早点下班回家睡觉呢。
别急下面就讲匹配包拦截。
二、初窥门径
看到within有这个匹配包或者类的功能,改造ProductService中insert()方法如下:
//@AdminOnly
public void insert(Product product) {
System.out.println("Insert product");
}
1. 不使用AdminOnly注解。
新写一个切面类PkgTypeAspectConfig 匹配类ProductService,这样执行到ProductService类中任何方法时就会进行权限校验。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.lw.service.AuthService;
@Aspect
@Component
public class PkgTypeAspectConfig {
@Autowired
AuthService authService;
@Pointcut("within(com.lw.service.ProductService)")
public void matchType() {
}
@Before("matchType()")
public void before() {
System.out.println("PkgTypeAspectConfig before");
authService.checkAccess();
}
}
看到此你想,“嗯,比我一个个添加注释好多了。”但是不要忽略了作为一个程序员想回家的激情,要是有很多类都需要添加权限校验呢?你难道写很多个如下方法?
@Pointcut(“within(com.lw.service.ProductService)”) public void matchType() { }
别逗,要是以后新增加了类呢?难道还要修改切面类?这样一看好像维护的东西多了,还能下班回家么?
答案是肯定能的,看下面。
@Pointcut(“within(com.lw.service.*)”)
这样就拦截com.lw.service包下所有类,但是不包括子类。
如果也要拦截子类需要使用
@Pointcut(“within(com.lw.service..*)”)
再考虑一点,如果我不仅仅要匹配一个类,一个包,而是匹配某个接口所有子类,怎么办?看下面匹配对象。
三、一窍不通
ProductService改造如下,实现一个接口IProductService。
@Service
public class ProductService implements IProductService
再写一个拦截IProductService接口的切面:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ObjectAspectConfig {
/**
* 拦截实现了接口IProductService的所有类
*/
@Pointcut("this(com.lw.service.IProductService)")
public void matchCondition() {
System.out.println("this");
}
/**
* 拦截实现了接口IProductService的所有类
*/
@Pointcut("target(com.lw.service.IProductService)")
public void matchCondition02() {
System.out.println("target");
}
/**
* 只拦截ProductService,相当于within(com.lw.service.ProductService)
*/
@Pointcut("bean(ProductService)")
public void matchCondition03() {
}
@Before("matchCondition()")
public void before() {
System.out.println("ObjectAspectConfig before");
}
}
有两种实现方式,this,target,至于区别,我他丫也懵逼。OK,反正能实现接口拦截。所以第三个标题为一窍不通。
此处有一点注意,如果IProductService接口实现了insert方法,那么SpringDataTest.java中就需要将ProductService类改为IProductService注入,否则报错,如下
@Autowired
IProductService productService;
public interface IProductService {
// public void insert(Product product);
}
四、四通八达
这里有匹配参数那么如何拦截ProductService中insert方法呢?
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ArgsAspectConfig {
/**
* 拦截ProductService类中方法参数为Product的方法
*/
@Pointcut("args(com.lw.domain.Product) && within(com.lw.service.ProductService)")
public void matchArgs() {
}
@Before("matchArgs()")
public void before() {
System.out.println("ArgsAspectConfig before");
}
}
看这个切面类,此切面类如果是自定义类需要args(com.lw.domain.Product)写成这样,否则光写个Product不识别。
注意这个@args也可以表现为传入的参数有@Repository注解的拦截,也就是说入参有此注解就会拦截。
好家伙,这个execution()拦截牛逼了,大家用了都说好。因为他支持类似正则表达式的表示方式。当然理解每个字段含义就不难。
首先modifier-pattern? 表示方法修饰符 public private protected,?表示非必选,可有可无。
ret-type-pattern 表示方法返回类型,比如com.lw.domain.Product,当然对于Java类型直接写String,Long,Integer等就可以了。
declaring-type-pattern? 表示方法所在的包,虽然是?,但是这个一般不会省略。
name-pattern 指方法名
param-pattern 指方法入参
throws-pattern? 表示方法抛出异常
说了这么多废话,感觉还不如来个列子实在。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ExecutionAspectConfig {
/**
* 拦截com.lw.service包下以Service结尾的类,方法是 public 任意返回值,参数任意
*/
@Pointcut("execution(public * com.lw.service.*Service.*(..))")
public void matchCondition() {
}
@Before("matchCondition()")
public void before() {
System.out.println("ExecutionAspectConfig before");
}
}
@AfterReturning 拦截的方法有返回值,就执行此注解,入参是方法返回的值
@AfterThrowing 拦截的方法抛出了异常,执行此注解
重点讲解下@Around注解
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AdviceAspectConfig {
/**
* 匹配任意返回值,com.lw.service.ProductService此类的任意方法,任意入参
*/
@Pointcut("execution(* com.lw.service.ProductService.*(..))")
public void matchCondition() {}
@Around("matchCondition()")
public Object after(ProceedingJoinPoint joinPoint) {
System.out.println("AdviceAspectConfig before");
Object result = null;
try {
result = joinPoint.proceed(joinPoint.getArgs());
System.out.println("AdviceAspectConfig return");
}catch(Throwable e) {
System.out.println(e.getMessage());
}finally {
System.out.println("AdviceAspectConfig finally");
}
return result;
}
}
执行结果如下:
AdviceAspectConfig before
Insert product
AdviceAspectConfig return
AdviceAspectConfig finally
哇,闭幕谢礼,虽然讲得很粗糙,但是项目中应用也就是这么粗糙,将错就错吧。
五、锦上添花
介绍一点基础小知识,关于线程的。
public class CurrentUserHolder {
private static final ThreadLocal<String> holder= new ThreadLocal<>();
public static String get() {
return holder.get()==null? "unknown":holder.get();
}
public static void set(String user) {
holder.set(user);
}
}
SpringDataTest.java中:
@Test
public void adminInsertTest() {
CurrentUserHolder.set("admin");
productService.insert(null);
}
AuthService.java类:
import org.springframework.stereotype.Component;
import com.lw.security.CurrentUserHolder;
/**
* 权限校验类
* @author king
*
*/
@Component
public class AuthService {
public void checkAccess() {
String user = CurrentUserHolder.get();
if(!"admin".equals(user)) {
throw new RuntimeException(String.format("用户%s没有权限!", user));
}
}
}
看到什么没,在SpringDataTest.java中设置的线程变量,可以在AuthService类中获取,线程全局变量啊。
六、管中窥豹
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<!-- spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--only use for class SpringAopTest test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
学习视频:https://www.imooc.com/learn/869