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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章