- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
AOP,面向切面编程,作为面向对象的一种补充,将公共逻辑(事务管理、日志、缓存、权限控制、限流等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码和降低模块之间的耦合度。切面就是那些与业务无关,但所有业务模块都会调用的公共逻辑.
先看一个例子:如何给如下UserServiceImpl中所有方法添加进入方法的日志, 。
public class UserServiceImpl implements IUserService {
@Override
public List<User> findUserList() {
System.out.println("execute method: findUserList");
return Collections.singletonList(new User("seven", 18));
}
@Override
public void addUser() {
System.out.println("execute method: addUser");
// do something
}
}
将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中! 。
OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。 AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异.
首先要知道,aop不是spring所特有的,同样的,这些术语也不是spring所特有的。是由AOP联盟定义的 。
切面(Aspect):切面是增强和切点的结合,增强和切点共同定义了切面的全部内容。 多个切面之间的执行顺序如何控制?首先要明确,在“进入”连接点的情况下,最高优先级的增强会先执行;在“退出”连接点的情况下,最高优先级的增强会最后执行.
连接点(Join point):一般指方法,在Spring AOP中,一个连接点总是代表一个方法的执行。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。当然,连接点也可能是类初始化、方法执行、方法调用、字段调用或处理异常等 。
增强(或称为通知)(Advice):在AOP术语中,切面的工作被称为增强。知实际上是程序运行时要通过Spring AOP框架来触发的代码段.
切点(Pointcut):切点的定义会匹配增强所要织入的一个或多个连接点。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。以AspectJ举例,说白了就可以理解为是execution表达式 。
引入(Introduction):引入允许我们向现有类添加新方法或属性。 在AOP中表示为干什么(引入什么); 。
目标对象(Target Object): 被一个或者多个切面(aspect)所增强(advise)的对象。它通常是一个代理对象.
织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在AOP中表示为怎么实现的;织入分为编译期织入、类加载期织入、运行期织入;SpringAOP是在运行期织入 。
execution表达式格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
*
,它代表了匹配任意的返回类型。*
通配符作为所有或者部分命名模式。*
)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。例如:
execution(* com.seven.springframeworkaopannojdk.service.*.*(..))
AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)。可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易.
下表总结了 Spring AOP 和 AspectJ 之间的关键区别
Spring AOP | AspectJ |
---|---|
在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
功能不强 - 仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等......。 |
只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
仅支持方法执行切入点 | 支持所有切入点 |
代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
比 AspectJ 慢多了 | 更好的性能 |
易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
AOP有两种实现方式:静态代理和动态代理.
静态代理分为:编译时织入(特殊编译器实现)、类加载时织入(特殊的类加载器实现).
代理类在编译阶段生成,在编译阶段将增强织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理.
缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护.
动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类 。
而Spring的AOP的实现就是通过动态代理实现的.
如果为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理.
InvocationHandler
接口和Proxy
类。那么什么时候采用哪种动态代理呢?
Spring提供了使用"aop"命名空间来定义一个切面,我们来看个例子 。
public class AopDemoServiceImpl {
public void doMethod1() {
System.out.println("AopDemoServiceImpl.doMethod1()");
}
public String doMethod2() {
System.out.println("AopDemoServiceImpl.doMethod2()");
return "hello world";
}
public String doMethod3() throws Exception {
System.out.println("AopDemoServiceImpl.doMethod3()");
throw new Exception("some exception");
}
}
public class LogAspect {
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----------------------");
System.out.println("环绕通知: 进入方法");
Object o = pjp.proceed();
System.out.println("环绕通知: 退出方法");
return o;
}
public void doBefore() {
System.out.println("前置通知");
}
public void doAfterReturning(String result) {
System.out.println("后置通知, 返回值: " + result);
}
public void doAfterThrowing(Exception e) {
System.out.println("异常通知, 异常: " + e.getMessage());
}
public void doAfter() {
System.out.println("最终通知");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<context:component-scan base-package="com.seven.springframeworkaopxml" />
<aop:aspectj-autoproxy/>
<!-- 目标类 -->
<bean id="demoService" class="com.seven.springframeworkaopxml.service.AopDemoServiceImpl">
<!-- configure properties of bean here as normal -->
</bean>
<!-- 切面 -->
<bean id="logAspect" class="com.seven.springframeworkaopxml.aspect.LogAspect">
<!-- configure properties of aspect here as normal -->
</bean>
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="logAspect">
<!-- 配置切入点 -->
<aop:pointcut id="pointCutMethod" expression="execution(* com.seven.springframeworkaopxml.service.*.*(..))"/>
<!-- 环绕通知 -->
<aop:around method="doAround" pointcut-ref="pointCutMethod"/>
<!-- 前置通知 -->
<aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
<!-- 后置通知;returning属性:用于设置后置通知的第二个参数的名称,类型是Object -->
<aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
<!-- 异常通知:如果没有异常,将不会执行增强;throwing属性:用于设置通知第二个参数的的名称、类型-->
<aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
<!-- 最终通知 -->
<aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
</aop:aspect>
</aop:config>
</beans>
public static void main(String[] args) {
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml");
// retrieve configured instance
AopDemoServiceImpl service = context.getBean("demoService", AopDemoServiceImpl.class);
// use configured instance
service.doMethod1();
service.doMethod2();
try {
service.doMethod3();
} catch (Exception e) {
// e.printStackTrace();
}
}
基于XML的声明式AspectJ存在一些不足,需要在Spring配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解.
注解名称 | 解释 |
---|---|
@Aspect | 用来定义一个切面。 |
@pointcut | 用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。 |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@After-Throwing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。 |
@After | 用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor (不要求掌握)。 |
基于JDK动态代理例子源码点这里 。
public interface IJdkProxyService {
void doMethod1();
String doMethod2();
String doMethod3() throws Exception;
}
@Service
public class JdkProxyDemoServiceImpl implements IJdkProxyService {
@Override
public void doMethod1() {
System.out.println("JdkProxyServiceImpl.doMethod1()");
}
@Override
public String doMethod2() {
System.out.println("JdkProxyServiceImpl.doMethod2()");
return "hello world";
}
@Override
public String doMethod3() throws Exception {
System.out.println("JdkProxyServiceImpl.doMethod3()");
throw new Exception("some exception");
}
}
@EnableAspectJAutoProxy
@Component
@Aspect
public class LogAspect {
/**
* define point cut.
*/
@Pointcut("execution(* com.seven.springframeworkaopannojdk.service.*.*(..))")
private void pointCutMethod() {
}
/**
* 环绕通知.
*
* @param pjp pjp
* @return obj
* @throws Throwable exception
*/
@Around("pointCutMethod()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----------------------");
System.out.println("环绕通知: 进入方法");
Object o = pjp.proceed();
System.out.println("环绕通知: 退出方法");
return o;
}
/**
* 前置通知.
*/
@Before("pointCutMethod()")
public void doBefore() {
System.out.println("前置通知");
}
/**
* 后置通知.
*
* @param result return val
*/
@AfterReturning(pointcut = "pointCutMethod()", returning = "result")
public void doAfterReturning(String result) {
System.out.println("后置通知, 返回值: " + result);
}
/**
* 异常通知.
*
* @param e exception
*/
@AfterThrowing(pointcut = "pointCutMethod()", throwing = "e")
public void doAfterThrowing(Exception e) {
System.out.println("异常通知, 异常: " + e.getMessage());
}
/**
* 最终通知.
*/
@After("pointCutMethod()")
public void doAfter() {
System.out.println("最终通知");
}
}
public class App {
public static void main(String[] args) {
// create and configure beans
ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannojdk");
// retrieve configured instance
IJdkProxyService service = context.getBean(IJdkProxyService.class);
// use configured instance
service.doMethod1();
service.doMethod2();
try {
service.doMethod3();
} catch (Exception e) {
// e.printStackTrace();
}
}
}
基于Cglib代理例子源码点这里 。
@Service
public class CglibProxyDemoServiceImpl {
public void doMethod1() {
System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
}
public String doMethod2() {
System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
return "hello world";
}
public String doMethod3() throws Exception {
System.out.println("CglibProxyDemoServiceImpl.doMethod3()");
throw new Exception("some exception");
}
}
和上面相同 。
public class App {
public static void main(String[] args) {
// create and configure beans
ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannocglib");
// cglib proxy demo
CglibProxyDemoServiceImpl service = context.getBean(CglibProxyDemoServiceImpl.class);
service.doMethod1();
service.doMethod2();
try {
service.doMethod3();
} catch (Exception e) {
// e.printStackTrace();
}
}
}
上面使用AspectJ的注解,并配合一个复杂的execution(* com.seven.springframeworkaopannojdk.service.*.*(..)) 语法来定义应该如何装配AOP。还有另一种方式,则是使用注解来装配AOP,这两者一般存在与不同的应用场景中:
对于业务开发来说,一般使用 注解的方式来装配AOP,因为如果要使用AOP进行增强,业务开发就需要配置注解,业务能够很好的感知到这个方法(这个类)进行了增强。如果使用 表达式来装配AOP,当后续新增Bean,如果不清楚现有的AOP装配规则,容易被强迫装配,而在开发时未感知到,导致出现线上故障。例如,Spring提供的@Transactional就是一个非常好的例子。如果自己写的Bean希望在一个数据库事务中被调用,就标注上@Transactional.
对于基础架构开发来说,无需业务感知到增强了什么方法,则可以使用表达式的方式来装配AOP。需要记录所有接口的耗时时长,直接写表达式,对业务无侵入 。
定义注解 。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAspectAnno {
}
@EnableAspectJAutoProxy
@Component
@Aspect
public class LogAspect {
@Around("@annotation(logaspectanno)") //注意,括号里为logaspectanno,而不是LogAspectAnno
public Object doAround(ProceedingJoinPoint pjp, LogAspectAnno logaspectanno) throws Throwable {
System.out.println("-----------------------");
System.out.println("环绕通知: 进入方法");
Object o = pjp.proceed();
System.out.println("环绕通知: 退出方法");
return o;
}
}
@Service
public class CglibProxyDemoServiceImpl {
@LogAspectAnno()
public void doMethod1() {
System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
}
public String doMethod2() {
System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
return "hello world";
}
}
@Service
public class JdkProxyDemoServiceImpl implements IJdkProxyService {
@LogAspectAnno
@Override
public void doMethod1() {
System.out.println("JdkProxyServiceImpl.doMethod1()");
}
@Override
public String doMethod2() {
System.out.println("JdkProxyServiceImpl.doMethod2()");
return "hello world";
}
}
// create and configure beans
ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannotation");
// cglib proxy demo
CglibProxyDemoServiceImpl service1 = context.getBean(CglibProxyDemoServiceImpl.class);
service1.doMethod1();
service1.doMethod2();
IJdkProxyService service2 = context.getBean(IJdkProxyService.class);
service2.doMethod1();
service2.doMethod2();
-----------------------
环绕通知: 进入方法
CglibProxyDemoServiceImpl.doMethod1()
环绕通知: 退出方法
CglibProxyDemoServiceImpl.doMethod2()
-----------------------
环绕通知: 进入方法
JdkProxyServiceImpl.doMethod1()
环绕通知: 退出方法
JdkProxyServiceImpl.doMethod2()
可以看到,只有doMethod1方法被增强了,doMethod2没有被增强,就是因为@LogAspectAnno 只注解了 doMethod1() 方法,从而实现更精细化的控制,是业务感知到这个方法是被增强了.
我们知道AO能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,提高系统可拓展性和可维护性.
@PreAuthorize
实现权限控制,其底层也是基于 AOP。利用 AOP 方式记录日志,只需要在 Controller 的方法上使用自定义 @Log 日志注解,就可以将用户操作记录到数据库.
@Log(description = "新增用户")
@PostMapping(value = "/users")
public ResponseEntity create(@Validated @RequestBody User resources){
checkLevel(resources);
return new ResponseEntity(userService.create(resources),HttpStatus.CREATED);
}
AOP 切面类 LogAspect用来拦截带有 @Log 注解的方法并处理:
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
// 定义切点,拦截带有 @Log 注解的方法
@Pointcut("@annotation(com.example.annotation.Log)") // 这里需要根据你的实际包名修改
public void logPointcut() {
}
// 环绕通知,用于记录日志
@Around("logPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//...
}
}
利用 AOP 方式对接口进行限流,只需要在 Controller 的方法上使用自定义的 @RateLimit 限流注解即可.
/**
* 该接口 60 秒内最多只能访问 10 次,保存到 redis 的键名为 limit_test,
*/
@RateLimit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")
public int test() {
return ATOMIC_INTEGER.incrementAndGet();
}
AOP 切面类 RateLimitAspect用来拦截带有 @RateLimit 注解的方法并处理:
@Slf4j
@Aspect
public class RateLimitAspect {
// 拦截所有带有 @RateLimit 注解的方法
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
//...
}
}
关于限流实现这里多说一句,这里并没有自己写 Redis Lua 限流脚本,而是利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法.
Spring Security 使用 AOP 进行方法拦截。在实际调用 update 方法之前,Spring 会检查当前用户的权限,只有用户权限满足对应的条件才能执行.
@Log(description = "修改菜单")
@PutMapping(value = "/menus")
// 用户拥有 `admin`、`menu:edit` 权限中的任意一个就能能访问`update`方法
@PreAuthorize("hasAnyRole('admin','menu:edit')")
public ResponseEntity update(@Validated @RequestBody Menu resources){
//...
}
Java面试题专栏已上线,欢迎访问.
那么可以私信我,我会尽我所能帮助你.
最后此篇关于SpringAOP基础、快速入门的文章就讲到这里了,如果你想了解更多关于SpringAOP基础、快速入门的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
如何更改循环中变量的名称?比如 number1 、 number2 、 number3 、 number4 ? var array = [2,4,6,8] func ap ( number1: Int
我想设置 View 的背景颜色并在一定延迟后将其更改为另一种颜色。这是我的尝试方式: print("setting color 1") self.view.backgroundColor = UICo
我在使用 express-session 时遇到问题。 session 数据不会在请求之间持续存在。 正如您在下面的代码中看到的那样,/join 路由设置了一些 session 属性,但是当 /sur
我试图从叶渲染器获得一个非常简单的结果,用于快速 Steam 的 for 循环。 我正在上传叶文件 HTML,因为它不接受此处格式正确的代码 - 下面的pizza.swift代码- import
你们中有人有什么好的链接可以与我分享吗?我正在寻找一个 FAST 程序员编辑器,它可以非常快速地打开包含超过 100, 000 行代码的文件?我目前正在使用记事本自动取款机,打开一个 29000 行长
我现在正在处理眼动追踪数据,因此拥有一个巨大的数据集(想想数百万行),因此希望有一种快速的方法来完成此任务。这是它的简化版本。 数据告诉您眼睛在每个时间点正在查看的位置以及我们正在查看的每个文件。 X
我是新手,想为计时器或其他设备选择提示音。 如何打开此列表,以选择其中一种声音? Alert sound list 最佳答案 您将无法在应用中使用系统声音。 但是,您可以包括自己的声音文件,并将其显示
我编写了以下代码来构建具有顺序字符串的数组。 它的工作方式与我预期的一样,但我希望它能更快地运行。有没有更有效的方法在PowerShell中产生我想要的结果? 我是PowerShell的新手,非常感谢
我有一个包含一些非唯一行的矩阵,例如: x 尝试 y <- rle(apply(x, 1, paste, collapse = " ")) # y$lengths is the vector con
我的函数“keyboardWillShown”有问题。所以我想要的是菜单打开时,菜单正好出现在键盘上方。它可以在Iphone 8 plus,8、7、6上完美运行。但是,当我在模拟器上运行Iphone
我正在尝试通过Swift 5中的HTTP get方法从API提取数据。它在启动时成功加载了数据,但是当我刷新页面时,它说“索引超出范围”,这是因为数据是不再会在我的日志中读取,因此索引中没有任何内容。
我想做什么: 从我的数据库中获取时间戳并将其转换为用户的时区。 我的代码: let tryItNow = "\(model.timestampName)" let format = D
给定字体名称和字体大小,如何查找字符串的宽度(CGFloat)? (目标是将UIView的宽度设置为足以容纳字符串的宽度。) 我有两个字符串:一个重复“1”,重复36次,另一个重复“M”,重复36次。
我正在尝试解析此JSON ["Items": ( { AccountBalance = 0; AlphabetType = 3; Description = "\U0631\U
我在UINavigationBar内放置了一个UILabel。 我想根据navigationBar的高度增加该标签的字体大小。当navigationBar很大时,我希望字体大小更大;当滚动并缩小nav
我想将用户输入限制为仅有效数字并使用以下内容: func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, rep
目前我有一个包含超过 100.000 张图像的数据库,它们大小不一或类似,但我想为我的公司制作以下内容: 我插入/上传一张图片,系统返回最有可能相同的图片。我不知道使用什么算法,但它需要快速。我可以预
在我的 swift 项目中,我有一个按钮,我想在标签上打印按下该按钮的时间。 如何解决这个问题? 最佳答案 添加到DHEERAJ的答案中,您只需在func press(sender: UIButton
我必须发表评论,尝试在解析中导入数组。然而,有一个问题。 当我尝试从 Parse 加载数组时,我的输出是 ("Blah","Blah","Blah")这是一个元组...而不是一个数组 TT... 如何
我的应用程序有一个名为 MyDevice 的类,我用它来与硬件通信。该硬件是可选的,实例变量也是可选的: var theDevice:MyDevice = nil 然后,在应用程序中,我必须初始化设备
我是一名优秀的程序员,十分优秀!