AOP Aspect Oriented Programing
面向切面编程。
AOP
是一种编程思想,不是一项技术。
AOP
使用横向抽取机制实现代码复用,继承方式是纵向的重复性代码。
AOP 思想: 基于代理,对原来目标对象,创建代理对象,在代理对象中,对原有业务方法进行增强。
AOP 实现方式: 过滤器、拦截器、切面。
AOP实际企业应用 : 打印方法运行时间、统一打印入参和出参、事务管理、日志记录、 权限控制、 缓存。
AOP在项目中的应用: 判断用户是否登录、统一打印入参和出参、记录请求运行时间。
AOP
需要对目标进行代理对象创建, Spring AOP
采用哪种技术对目标进行代理对象的创建: JDK动态代理、CGLIB动态代理
。
JDK 动态代理: 针对接口生成代理。
CGLIB 动态代理 直接对目标对象(可以没有接口)生成代理对象,原理: 对目标类创建子类对象。
Spring AOP的代理小结:
过滤器 | 拦截器 | Aspect | |
---|---|---|---|
关注的点 | 所有web请求 | 部分web请求 | 偏向于业务层面的拦截 |
实现原理 | 函数回调 | JAVA反射机制(动态代理) | 动态代理 |
Servlet提供的支持 | Filter接口 | 无 | 无 |
Spring提供的支持 | 无 | HandlerInterceptorAdapter类、HandlerInterceptor接口 | @Aspect |
可能需要实现的方法 | void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) | boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | Object around(ProceedingJoinPoint joinPoint) |
简单来讲就是用来过滤东西的,比如:统一设置编码,统一设置所有请求头信息。
实现: 实现 javax.Servlet.Filter
接口就可以创建一个过滤器。
/**
* 统一记录请求耗时
*/
@Slf4j
@WebFilter(filterName = "ApiAccessFilter", urlPatterns = "/*")
public class ApiAccessFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
long start = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
long duration = System.currentTimeMillis() - start;
if (duration > 200) {
log.info("Api Access uri: {}, method: {}, client: {}, duration: {}ms , time is too long",
request.getRequestURI(), request.getMethod(), getIP(request), System.currentTimeMillis() - start);
}else{
log.info("Api Access uri: {}, method: {}, client: {}, duration: {}ms",
request.getRequestURI(), request.getMethod(), getIP(request), System.currentTimeMillis() - start);
}
}
private String getIP(HttpServletRequest request) {
if (request == null) {
return "0.0.0.0";
}
String Xip = request.getHeader("X-Real-IP");
String XFor = request.getHeader("X-Forwarded-For");
String UNKNOWN_IP = "unknown";
if (StringUtils.isNotEmpty(XFor) && !UNKNOWN_IP.equalsIgnoreCase(XFor)) {
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = XFor.indexOf(",");
if (index != -1) {
return XFor.substring(0, index);
} else {
return XFor;
}
}
XFor = Xip;
if (StringUtils.isNotEmpty(XFor) && !UNKNOWN_IP.equalsIgnoreCase(XFor)) {
return XFor;
}
if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
XFor = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
XFor = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
XFor = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
XFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(XFor) || UNKNOWN_IP.equalsIgnoreCase(XFor)) {
XFor = request.getRemoteAddr();
}
return XFor;
}
}
在执行方法之前,进行拦截,然后在之前或之后加入一些操作。
使用场景:
实现拦截器: 实现 HandlerInterceptor
接口就可以创建一个拦截器。
HandlerInterceptor
接口的源码如下:
public interface HandlerInterceptor {
// 在请求处理之前进行调用(Controller方法调用之前)
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
// 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后),如果异常发生,则该方法不会被调用
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 在整个请求结束之后被调用,也就是在 DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotCheckToken {
}
@Component
public class AuthenticationIntercept extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
NotCheckToken methodAnnotation = method.getMethodAnnotation(NotCheckToken.class);
if (Objects.nonNull(methodAnnotation)) {
return true;
}
}
String userId = request.getHeader("userId");
if (StringUtils.isBlank(userId)) {
throw new Exception("未登录");
}
request.setAttribute("userId", userId);
return super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
}
拦截器注册:
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
@Autowired
private AuthenticationIntercept authenticationIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration registration = registry.addInterceptor(authenticationIntercept);
// 拦截所有请求
registration.addPathPatterns("/**");
// 添加不拦截路径
registration.excludePathPatterns("/login", "/error", "/logout","/login.html");
}
}
AspectJ
是第三方 AOP
框架, 现阶段企业 AOP
开发,使用 AspectJ
。
AspectJ 提供的几种Advice类型 :
应用场景:
日志记录、权限控制。应用场景:
和业务相关, 网上营业厅查询余额后,自动下发短信。应用场景:
日志、缓存、权限、性能监控、 事务管理。应用场景:
处理异常 ,记录日志。应用场景 :
关闭和释放一些资源@Aspect: 标识为切面类,被容器识别。
@Pointcut: 配置切入点:
com.demo.controller
包下的所有类型所有方法:@Pointcut(value = "execution(* com.demo.controller.*.*(..))")
com.demo.controller
包和子包下的所有类型所有方法:@Pointcut(value = "execution(* com.demo.controller..*.*(..))")
com.demo.controller
包下 DemoController
类型下的所有方法:@Pointcut(value = "execution(* com.demo.controller.DemoController.*.*(..))")
Inter
注解就会拦截:@Pointcut(value = "@annotation(com.demo.Inter)")
Inter
注解就会拦截:@Pointcut(value = "@within(com.demo.Inter)")
@Pointcut(value = "@within(com.demo.Inter) && @annotation(com.demo.Inter)")
@Pointcut(value = "@within(com.demo.Inter) || @annotation(com.demo.Inter)")
1、将业务逻辑组件和切面类都加入到容器中;告诉Spring哪个是切面类(@Aspect)。
2、在切面类上的每一个通知方法上标注通知注解,告诉Spring何时何地运行(切入点表达式)。
3、开启基于注解的aop模式;@EnableAspectJAutoProxy
1、导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
2、编写切面类
@Aspect
@Component
@Slf4j
public class WebLogAspect {
//切入点描述 这个是controller包的切入点
@Pointcut("execution(public * com.example.demo.controller..*.*(..))")
//签名,可以理解成这个切入点的一个名称
public void controllerLog(){}
@Pointcut("execution(public * com.example.demo.service..*.*(..))")
public void serviceLog(){}
//在切入点的方法run之前要干的
@Before("controllerLog()||serviceLog()")
public void logBeforeController(JoinPoint joinPoint) {
//这个RequestContextHolder是SpringMvc提供来获得请求的东西
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
// 记录下请求内容
log.info("################URL : " + request.getRequestURL().toString());
}
//在切入点的方法run之后要干的
@After("controllerLog()||serviceLog()")
public void logAfterController(JoinPoint joinPoint) {
System.out.println("after");
}
}
切面类示例
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(public * com.example.controller.*.*(..))")
public void webLog(){}
@Before("webLog()")
public void deBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
System.out.println("URL : " + request.getRequestURL().toString());
System.out.println("HTTP_METHOD : " + request.getMethod());
System.out.println("IP : " + request.getRemoteAddr());
System.out.println("CLASS_METHOD : " +
joinPoint.getSignature().getDeclaringTypeName() +
"." + joinPoint.getSignature().getName());
System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
System.out.println("方法的返回值 : " + ret);
}
//后置异常通知
@AfterThrowing("webLog()")
public void throwss(JoinPoint jp){
System.out.println("方法异常时执行.....");
}
//后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
@After("webLog()")
public void after(JoinPoint jp){
System.out.println("方法最后执行.....");
}
//环绕通知,环绕增强,相当于MethodInterceptor
@Around("webLog()")
public Object arround(ProceedingJoinPoint pjp) {
System.out.println("方法环绕start.....");
try {
Object o = pjp.proceed();
System.out.println("方法环绕proceed,结果是 :" + o);
return o;
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
}
JoinPoint
对象封装了SpringAop中切面方法的信息,在切面方法中添加 JoinPoint 参数,就可以获取到封装了该方法信息的JoinPoint对象。
常用API:
方法名 | 功能 |
---|---|
Signature getSignature() | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs() | 获取传入目标方法的参数对象 |
Object getTarget() | 获取被代理的对象 |
Object getThis() | 获取代理对象 |
System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "个参数为:" + args[i]);
}
System.out.println("被代理的对象:" + joinPoint.getTarget());
System.out.println("代理对象自己:" + joinPoint.getThis());
Class<?> classTarget = joinPoint.getTarget().getClass();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method objMethod = classTarget.getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
GpDbSetAppAnnotation annotation = objMethod.getAnnotation(GpDbSetAppAnnotation.class);
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class WebAspect {
@Pointcut("execution(public * com.example.controller.*.*(..))")
public void pointCut() {
}
@AfterReturning(value = "pointCut()", returning = "returnVal")
public void afterReturning(JoinPoint joinPoint, Object returnVal) {
log.info("{} after return, returnVal: {}", joinPoint.getSignature().getName(), returnVal);
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getName();
Object[] args = joinPoint.getArgs();
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map<String, Object> paramMap = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
log.info("path:{} {}.{} start, param:{}", request.getServletPath(), className, methodName, paramMap.toString());
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error("around error", e);
}
long endTime = System.currentTimeMillis();
log.info("{}.{} end execute time:{} ms", className, methodName, endTime - startTime);
return result;
}
}
业务类。
public interface HelloService {
public void sayHello(String name);
}
//
import com.demo.service.HelloService;
import org.springframework.stereotype.Service;
@Service
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello(String name) {
if (name == null || name.trim() == "") {
throw new RuntimeException ("parameter is null!!");
}
System.out.println("hello " + name);
}
}
定义一个以反射的形式去调用原有的方法工具。
import lombok.Getter;
import lombok.Setter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@Getter
@Setter
public class Invocation {
private Object[] params;
private Method method;
private Object target;
public Invocation(Object target, Method method, Object[] params) {
this.target = target;
this.method = method;
this.params = params;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, params);
}
}
定义一个自己的拦截器。
import com.demo.invoke.Invocation;
import java.lang.reflect.InvocationTargetException;
public interface Interceptor {
//事前方法
public boolean before();
//事后方法
public void after();
/**
* 取代原有事件方法
* @param invocation -- 回调参数,可以通过它的proceed方法,回调原有事件
* @return 原有事件返回对象
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
//是否返回方法。事件没有发生异常执行
public void afterReturning();
//事后异常方法,当事件发生异常后执行
public void afterThrowing();
//是否使用around方法取代原有方法
boolean useAround();
}
///
import com.demo.invoke.Invocation;
import java.lang.reflect.InvocationTargetException;
public class MyInterceptor implements Interceptor {
@Override
public boolean before() {
System.out.println("before ......");
return true;
}
@Override
public boolean useAround() {
return true;
}
@Override
public void after() {
System.out.println("after ......");
}
@Override
public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
System.out.println("around before ......");
Object obj = invocation.proceed();
System.out.println("around after ......");
return obj;
}
@Override
public void afterReturning() {
System.out.println("afterReturning......");
}
@Override
public void afterThrowing() {
System.out.println("afterThrowing 。。。。。。");
}
}
编写生成代理业务类。
import com.demo.intercept.Interceptor;
import com.demo.invoke.Invocation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyBean implements InvocationHandler {
private Object target = null;
private Interceptor interceptor = null;
/**
* 绑定代理对象
* @param target 被代理对象
* @param interceptor 拦截器
* @return 代理对象
*/
public static Object getProxyBean(Object target, Interceptor interceptor) {
ProxyBean proxyBean = new ProxyBean();
// 保存被代理对象
proxyBean.target = target;
// 保存拦截器
proxyBean.interceptor = interceptor;
// 生成代理对象
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
proxyBean);
// 返回代理对象
return proxy;
}
/**
* 处理代理对象方法逻辑
* @param proxy 代理对象
* @param method 当前方法
* @param args 运行参数
* @return 方法调用结果
* @throws Throwable 异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
//异常标识
boolean exceptionFlag = false;
Invocation invocation = new Invocation(target, method, args);
Object retObj = null;
try {
if (this.interceptor.before()) {
retObj = this.interceptor.around(invocation);
} else {
retObj = method.invoke(target, args);
}
} catch (Exception ex) {
//产生异常
exceptionFlag = true;
}
this.interceptor.after();
if (exceptionFlag) {
this.interceptor.afterThrowing();
} else {
this.interceptor.afterReturning();
return retObj;
}
return null;
}
}
测试类。
import com.demo.intercept.MyInterceptor;
import com.demo.proxy.ProxyBean;
import com.demo.service.HelloService;
import com.demo.service.impl.HelloServiceImpl;
public class AopMain {
public static void main(String[] args) {
HelloService helloService = new HelloServiceImpl();
HelloService proxy = (HelloService) ProxyBean.getProxyBean(helloService, new MyInterceptor());
proxy.sayHello("Tom");
}
}
测试结果。
before ......
around before ......
hello Tom
around after ......
after ......
afterReturning......
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Prevent {
int seconds() default 60;
int maxCount() default 1;
String message() default "";
}
import cn.hutool.json.JSONUtil;
import com.boot.common.BusinessException;
import org.aspectj.lang.JoinPoint;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
/**
* 防刷切面实现类
*/
@Aspect
@Component
public class PreventAop {
@Autowired
private StringRedisTemplate redisTemplate;
@Pointcut("@annotation(com.boot.aop.Prevent)")
public void pointcut() {
}
@Before("pointcut()")
public void joinPoint(JoinPoint joinPoint) throws Exception {
String requestStr = JSONUtil.toJsonStr(joinPoint.getArgs()[0]);
if (StringUtils.isEmpty(requestStr) || requestStr.equalsIgnoreCase("{}")) {
throw new BusinessException("[防刷]入参不允许为空");
}
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(methodSignature.getName(),
methodSignature.getParameterTypes());
Prevent preventAnnotation = method.getAnnotation(Prevent.class);
String methodFullName = method.getDeclaringClass().getName() + method.getName();
defaultHandle(requestStr, preventAnnotation, methodFullName);
}
private void defaultHandle(String requestStr, Prevent prevent, String methodFullName) throws Exception {
String base64Str = toBase64String(requestStr);
int expire = prevent.seconds();
int maxCount = prevent.maxCount();
String resp = redisTemplate.opsForValue().get(methodFullName + base64Str);
int count = StringUtils.isEmpty(resp) ? 0 : Integer.parseInt(resp);
if (StringUtils.isEmpty(resp)) {
redisTemplate.opsForValue().set(methodFullName + base64Str, "1", expire, TimeUnit.SECONDS);
} else if (count < maxCount) {
redisTemplate.opsForValue().increment(methodFullName + base64Str);
} else {
String message = !StringUtils.isEmpty(prevent.message()) ? prevent.message() :
expire + "超出访问次数";
throw new BusinessException(message);
}
}
/**
* 对象转换为base64字符串
*
* @param obj 对象值
* @return base64字符串
*/
private String toBase64String(String obj) throws Exception {
if (StringUtils.isEmpty(obj)) {
return null;
}
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = obj.getBytes(StandardCharsets.UTF_8);
return encoder.encodeToString(bytes);
}
}