聊一聊 AOP :表现形式与基础概念

聊一聊 AOP :表现形式与基础概念

aop 终于提上日程来写一写了。

本系列分为 上、中、下三篇。上篇主要是介绍如果使用 AOP ,提供了demo和配置方式说明;中篇来对实现 AOP 的技术原理进行分析;下篇主要针对Spring中对于AOP的实现进行源码分析。

项目地址项目地址:glmapper-ssm-parent

这个项目里面包含了下面几种 AOP 实现方式的所有代码,有兴趣的同学可以fork跑一下。这个demo中列举了4中方式的实现:

基于代码的方式基于纯POJO类的方式基于Aspect注解的方式基于注入式Aspect的方式目前我们经常用到的是基于Aspect注解的方式的方式。下面来一个个了解下不同方式的表现形式。

基于代理的方式这种方式看起来很好理解,但是配置起来相当麻烦;小伙伴们可以参考项目来看,这里只贴出比较关键的流程代码。

1、首先定义一个接口:GoodsService代码语言:javascript复制public interface GoodsService {

/**

* 查询所有商品信息

*

* @param offset 查询起始位置

* @param limit 查询条数

* @return

*/

List queryAll(int offset,int limit);

}

2、GoodsService 实现类代码语言:javascript复制@Service

@Qualifier("goodsService")

public class GoodsServiceImpl implements GoodsService {

@Autowired

private GoodsDao goodsDao;

public List queryAll(int offset, int limit) {

System.out.println("执行了queryAll方法");

List list = new ArrayList();

return list;

}

}

3、定义一个通知类 LoggerHelper,该类继承 MethodBeforeAdvice和 AfterReturningAdvice。代码语言:javascript复制//通知类 LoggerHelper

public class LoggerHelper implements MethodBeforeAdvice,

AfterReturningAdvice {

private static final Logger LOGGER = LoggerFactory.getLogger(LoggerHelper.class);

//MethodBeforeAdvice的before方法实现

public void before(Method method, Object[] objects, Object o) throws Throwable {

LOGGER.info("before current time:"+System.currentTimeMillis());

}

//AfterReturningAdvice的afterReturning方法实现

public void afterReturning(Object o, Method method,

Object[] objects, Object o1) throws Throwable {

LOGGER.info("afterReturning current time:"+System.currentTimeMillis());

}

}

4、重点,这个配置需要关注下。这个项目里面我是配置在applicationContext.xml文件中的。代码语言:javascript复制

5、使用:注解注入方式代码语言:javascript复制@Controller

@RequestMapping("/buy")

public class BuyController {

@Autowired

private OrderService orderService;

//因为我们已经在配置文件中配置了proxy,

//所以这里可以直接注入拿到我们的代理类

@Autowired

private GoodsService proxy;

@RequestMapping("/initPage")

public ModelAndView initPage(HttpServletRequest request,

HttpServletResponse response, ModelAndView view) {

//这里使用proxy执行了*query*,

List goods = proxy.queryAll(10,10);

view.addObject("goodsList", goods);

view.setViewName("goodslist");

return view;

}

}

6、使用:工具类方式手动获取bean这个方式是通过一个SpringContextUtil工具类来获取代理对象的。

代码语言:javascript复制@RequestMapping("/initPage")

public ModelAndView initPage(HttpServletRequest request,

HttpServletResponse response, ModelAndView view) {

//这里通过工具类来拿,效果一样的。

GoodsService proxy= (GoodsService) SpringContextUtil.getBean("proxy");

List goods = proxy.queryAll(10,10);

view.addObject("goodsList", goods);

view.setViewName("goodslist");

return view;

}

7、SpringContextUtil 类的定义这个还是有点坑的,首先SpringContextUtil是继承ApplicationContextAware这个接口,我们希望能够SpringContextUtil可以被Spring容器直接管理,所以,需要使用 @Component 标注。标注了之后最关键的是它得能够被我们配置的注入扫描扫到(亲自踩的坑,我把它放在一个扫不到的包下面,一直debug都是null;差点砸电脑…)

代码语言:javascript复制@Component

public class SpringContextUtil implements ApplicationContextAware {

// Spring应用上下文环境

private static ApplicationContext applicationContext;

/**

* 实现ApplicationContextAware接口的回调方法,设置上下文环境

*

* @param applicationContext

*/

public void setApplicationContext(ApplicationContext applicationContext) {

SpringContextUtil.applicationContext = applicationContext;

}

/**

* @return ApplicationContext

*/

public static ApplicationContext getApplicationContext() {

return applicationContext;

}

/**

* 获取对象

* 这里重写了bean方法,起主要作用

* @param name

* @return Object 一个以所给名字注册的bean的实例

* @throws BeansException

*/

public static Object getBean(String name) throws BeansException {

return applicationContext.getBean(name);

}

}

8、运行结果代码语言:javascript复制21:04:47.940 [http-nio-8080-exec-7] INFO

c.g.framerwork.aspect.LoggerHelper - before current

time:1529413487940

执行了queryAll方法

21:04:47.940 [http-nio-8080-exec-7] INFO

c.g.framerwork.aspect.LoggerHelper - afterReturning current

time:1529413487940

上面就是最最经典的方式,就是通过代理的方式来实现AOP的过程。

纯POJO切面注意这里和LoggerHelper的区别,这里的LoggerAspect并没有继承任何接口或者抽象类。

1、POJO 类定义代码语言:javascript复制/**

* @description: [描述文本]

* @email:

* @author: guolei.sgl

* @date: 18/6/20

*/

public class LoggerAspect {

private static final Logger LOGGER =

LoggerFactory.getLogger(LoggerHelper.class);

public void before(){

LOGGER.info("before current time:"+System.currentTimeMillis());

}

public void afterReturning() {

LOGGER.info("afterReturning current time:"+System.currentTimeMillis());

}

}

2、配置文件代码语言:javascript复制

class="com.glmapper.framerwork.aspect.LoggerAspect">

"execution(* com.glmapper.framerwork.service.impl.*.*(..)) " />

method="afterReturning"/>

注意这里LoggerAspect中的before和afterReturning如果有参数,这里需要处理下,否则会报 0 formal unbound in pointcut 异常。

@AspectJ 注解驱动方式这种方式是最简单的一种实现,直接使用 @Aspect 注解标注我们的切面类即可。

1、定义切面类,并使用 @Aspect 进行标注代码语言:javascript复制/**

* @description: 使用Aspect注解驱动的方式

* @email:

* @author: guolei.sgl

* @date: 18/6/20

*/

@Aspect

public class LoggerAspectInject {

private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectInject.class);

@Pointcut("execution(* com.glmapper.framerwork.service.impl.*.*(..))")

public void cutIn(){}

@Before("cutIn()")

public void before(){

LOGGER.info("before current time:"+System.currentTimeMillis());

}

@AfterReturning("cutIn()")

public void AfterReturning(){

LOGGER.info("afterReturning current time:"+System.currentTimeMillis());

}

}

2、使用方式1:配置文件方式声明 bean代码语言:javascript复制

class="com.glmapper.framerwork.aspect.LoggerAspectInject">

class="com.glmapper.framerwork.service.impl.GoodsServiceImpl">

3、客户端使用:代码语言:javascript复制@Controller

@RequestMapping("/buy")

public class BuyController {

@Autowired

private OrderService orderService;

@RequestMapping("/initPage")

public ModelAndView initPage(HttpServletRequest request,

HttpServletResponse response, ModelAndView view) {

//通过SpringContextUtil手动获取 代理bean

GoodsService goodsService=(GoodsService)

SpringContextUtil.getBean("goodsServiceImpl");

List goods = goodsService.queryAll(10,10);

view.addObject("goodsList", goods);

view.setViewName("goodslist");

return view;

}

}

4、使用方式2:使用@component注解托管给IOC代码语言:javascript复制@Aspect

@Component //这里加上了Component注解,就不需要在xml中配置了

public class LoggerAspectInject {

private static final Logger LOGGER =

LoggerFactory.getLogger(LoggerAspectInject.class);

@Pointcut("execution(* com.glmapper.framerwork.service.impl.*.*(..))")

public void cutIn(){}

@Before("cutIn()")

public void before(){

LOGGER.info("before current time:"+System.currentTimeMillis());

}

@AfterReturning("cutIn()")

public void AfterReturning(){

LOGGER.info("afterReturning current time:"+System.currentTimeMillis());

}

}

5、客户端代码:代码语言:javascript复制@Controller

@RequestMapping("/buy")

public class BuyController {

@Autowired

private OrderService orderService;

//直接注入

@Autowired

private GoodsService goodsService;

@RequestMapping("/initPage")

public ModelAndView initPage(HttpServletRequest request,

HttpServletResponse response, ModelAndView view) {

List goods = goodsService.queryAll(10,10);

view.addObject("goodsList", goods);

view.setViewName("goodslist");

return view;

}

}

6、比较完整的一个LoggerAspectInject,在实际工程中可以直接参考代码语言:javascript复制/**

* @description: aop

* @email:

* @author: glmapper@磊叔

* @date: 18/6/4

*/

@Aspect

@Component

public class LoggerAspectInject {

private static final Logger LOGGER= LoggerFactory.getLogger(LoggerAspectInject.class);

@Pointcut("execution(* com.glmapper.book.web.controller.*.*(..))")

public void cutIn(){

}

@Around("cutIn()") // 定义Pointcut,名称即下面的标识"aroundAdvice

public Object aroundAdvice(ProceedingJoinPoint poin){

System.out.println("环绕通知");

Object object = null;

try{

object = poin.proceed();

}catch (Throwable e){

e.printStackTrace();

}

return object;

}

// 定义 advise

//这个方法只是一个标识,相当于在配置文件中定义了pointcut的id,此方法没有返回值和参数

@Before("cutIn()")

public void beforeAdvice(){

System.out.println("前置通知");

}

@After("cutIn()")

public void afterAdvice(){

System.out.println("后置通知");

}

@AfterReturning("cutIn()")

public void afterReturning(){

System.out.println("后置返回 ");

}

@AfterThrowing("cutIn()")

public void afterThrowing(){

System.out.println("后置异常");

}

}

关于命名切入点:上面的例子中cutIn方法可以被称之为命名切入点,命名切入点可以被其他切入点引用,而匿名切入点是不可以的。只有@AspectJ支持命名切入点,而Schema风格不支持命名切入点。

如下所示,@AspectJ使用如下方式引用命名切入点:

代码语言:javascript复制@Pointcut("execution(* com.glmapper.book.web.controller.*.*(..))")

public void cutIn(){

}

//引入命名切入点

@Before("cutIn()")

public void beforeAdvice(){

System.out.println("前置通知");

}

注入式 AspectJ 切面这种方式我感觉是第二种和第三种的结合的一种方式。

1、定义切面类代码语言:javascript复制/**

* @description: 注入式 也是一种通过XML方式配置的方式

* @email:

* @author: guolei.sgl

* @date: 18/6/20

*/

public class LoggerAspectHelper {

private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectHelper.class);

/**

* 调动方法前执行

* @param point

* @throws Throwable

*/

public void doBefore(JoinPoint point) throws Throwable {

LOGGER.info("before current time:"+System.currentTimeMillis());

}

/**

* 在调用方法前后执行

* @param point

* @return

* @throws Throwable

*/

public Object doAround(ProceedingJoinPoint point) throws Throwable

{

LOGGER.info("around current time:"+System.currentTimeMillis());

if(point.getArgs().length>0) {

return point.proceed(point.getArgs());

}else{

return point.proceed();

}

}

/**

* 在调用方法之后执行

* @param point

* @throws Throwable

*/

public void doAfter(JoinPoint point) throws Throwable

{

LOGGER.info("after current time:"+System.currentTimeMillis());

}

/**

* 异常通知

* @param point

* @param ex

*/

public void doThrowing(JoinPoint point, Throwable ex)

{

LOGGER.info("throwing current time:"+System.currentTimeMillis());

}

}

2、XML 配置代码语言:javascript复制

class="com.glmapper.framerwork.aspect.LoggerAspectHelper">

"execution(* com.glmapper.framerwork.service.impl.*.*(..))" />

method="doThrowing" throwing="ex" />

3、结果代码语言:javascript复制23:39:48.756 [http-nio-8080-exec-4] INFO c.g.f.aspect.LoggerAspectHelper

- before current time:1529509188756

23:39:48.757 [http-nio-8080-exec-4] INFO c.g.f.aspect.LoggerAspectHelper

- around current time:1529509188757

excute queryAll method...

23:39:48.757 [http-nio-8080-exec-4] INFO c.g.f.aspect.LoggerAspectHelper

- after current time:1529509188757

## 表达式从上面的例子中我们都是使用一些正则表达式来指定我们的切入点的。在实际的使用中,不仅仅是execution,还有其他很多种类型的表达式。下面就列举一些:

1、execution用于匹配方法执行的连接点;

代码语言:javascript复制execution(* com.glmapper.book.web.controller.*.*(..))

execution()表达式的主体;第一个 "*" 符号表示返回值的类型任意;com.glmapper.book.web.controller AOP所切的服务的包名,即,我们的业务部分包名后面的"." 表示当前包及子包第二个"*" 表示类名,即所有类.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型2、within用于匹配指定类型内的方法执行;

代码语言:javascript复制//如果在com.glmapper.book.web.controller包或其下的任何子包中

//定义了该类型,则在Web层中有一个连接点。

within(com.glmapper.book.web.controller..*)

@Pointcut("within(com.glmapper.book.web.controller..*)")

public void cutIn(){}

@within:用于匹配所以持有指定注解类型内的方法;

代码语言:javascript复制/**

* @description: 注解定义

* @email:

* @author: glmapper@磊叔

* @date: 18/6/4

*/

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.METHOD,ElementType.FIELD})

public @interface AuthAnnotation {

}

任何目标对象对应的类型持有AuthAnnotation注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。

代码语言:javascript复制@within(com.glmapper.book.common.annotaion.AuthAnnotation)

//所有被@AdviceAnnotation标注的类都将匹配

@Pointcut("@within(com.glmapper.book.common.annotaion.AuthAnnotation)")

public void cutIn(){}

3、this用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;this中使用的表达式必须是类型全限定名,不支持通配符;

代码语言:javascript复制//当前目标对象(非AOP对象)实现了 UserService 接口的任何方法

this(com.glmapper.book.web.service.UserService)

//用于向通知方法中传入代理对象的引用。

@Before("cutIn() && this(proxy)")

public void beforeAdvice(ProceedingJoinPoint poin,Object proxy){

System.out.println("前置通知");

}

4、target用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;target中使用的表达式必须是类型全限定名,不支持通配符;

代码语言:javascript复制//当前目标对象(非AOP对象)实现了 UserService 接口的任何方法

target(com.glmapper.book.web.service.UserService)

//用于向通知方法中传入代理对象的引用。

@Before("cutIn() && target(proxy)")

public void beforeAdvice(ProceedingJoinPoint poin,Object proxy){

System.out.println("前置通知");

}

@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;任何目标对象持有Secure注解的类方法;这个和@within一样必须是在目标对象上声明这个注解,在接口上声明的对它同样不起作用。

代码语言:javascript复制@target(com.glmapper.book.common.annotaion.AuthAnnotation)

@Pointcut("@target(com.glmapper.book.common.annotaion.AuthAnnotation)")

public void cutIn(){}

5、args用于匹配当前执行的方法传入的参数为指定类型的执行方法;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;

代码语言:javascript复制//任何一个以接受“传入参数类型为java.io.Serializable”开头,

//且其后可跟任意个任意类型的参数的方法执行,

//args指定的参数类型是在运行时动态匹配的

args (java.io.Serializable,..)

//用于将参数传入到通知方法中。

@Before("cutIn() && args(age,username)")

public void beforeAdvide(JoinPoint point, int age, String username){

//...

}

@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解AuthAnnotation;动态切入点,类似于arg指示符;

代码语言:javascript复制@args (com.glmapper.book.common.annotaion.AuthAnnotation)

@Before("@args(com.glmapper.book.common.annotaion.AuthAnnotation)")

public void beforeAdvide(JoinPoint point){

//...

}

6、@annotation使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;

代码语言:javascript复制//当前执行方法上持有注解 AuthAnnotation将被匹配

@annotation(com.glmapper.book.common.annotaion.AuthAnnotation)

//匹配连接点被它参数指定的AuthAnnotation注解的方法。

//也就是说,所有被指定注解标注的方法都将匹配。

@Pointcut("@annotation(com.glmapper.book.common.annotaion.AuthAnnotation)")

public void cutIn(){}

还有一种是bean的方式,没用过。有兴趣可以看看。

例子在下面说到的基础概念部分对应给出。

基础概念基础概念部分主要将 AOP 中的一些概念点捋一捋,这部分主要参考了官网上的一些解释。

AOPAOP(Aspect-Oriented Programming), 即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中,我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)。

横切关注点(Cross Cutting Concern):独立服务,如系统日志。如果不是独立服务(就是与业务耦合比较强的服务)就不能横切了。通常这种独立服务需要遍布系统各个角落,遍布在业务流程之中。

Target Object目标对象。织入 advice 的目标对象。 目标对象也被称为 advised object。

因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object);注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类。

织入(Weave)即Advice应用在JoinPoint的过程,这个过程叫织入。从另外一个角度老说就是将 aspect 和其他对象连接起来, 并创建 adviced object 的过程。

根据不同的实现技术, AOP织入有三种方式:

编译器织入,这要求有特殊的Java编译器类装载期织入, 这需要有特殊的类装载器动态代理织入, 在运行期为目标类添加增强( Advice )生成子类的方式。Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期

代理Spring AOP默认使用代理的是标准的JDK动态代理。这使得任何接口(或一组接口)都可以代理。

Spring AOP也可以使用CGLIB代理。如果业务对象不实现接口,则默认使用CGLIB。对接口编程而不是对类编程是一种很好的做法;业务类通常会实现一个或多个业务接口。在一些特殊的情况下,即需要通知的接口上没有声明的方法,或者需要将代理对象传递给具体类型的方法,有可能强制使用CGLIB。

Introductions我们知道Java语言本身并非是动态的,就是我们的类一旦编译完成,就很难再为他添加新的功能。但是在一开始给出的例子中,虽然我们没有向对象中添加新的方法,但是已经向其中添加了新的功能。这种属于向现有的方法添加新的功能,那能不能为一个对象添加新的方法呢?答案肯定是可以的,使用introduction就能够实现。

introduction:动态为某个类增加或减少方法。为一个类型添加额外的方法或字段。Spring AOP 允许我们为 目标对象 引入新的接口(和对应的实现)。

Aspect切面:通知和切入点的结合。

切面实现了cross-cutting(横切)功能。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。

而将散落于各个业务对象之中的Cross-cutting concerns 收集起来,设计各个独立可重用的对象,这些对象称之为Aspect;在上面的例子中我们根据不同的配置方式,定义了四种不同形式的切面。

JoinpointAspect 在应用程序执行时加入业务流程的点或时机称之为 Joinpoint ,具体来说,就是 Advice 在应用程序中被呼叫执行的时机,这个时机可能是某个方法被呼叫之前或之后(或两者都有),或是某个异常发生的时候。

Joinpoint & ProceedingJoinPoint环绕通知 = 前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的。

环绕通知 ProceedingJoinPoint 执行 proceed 方法 的作用是让目标方法执行 ,这 也是环绕通知和前置、后置通知方法的一个最大区别。

Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法;暴露出这个方法,就能支持这种切面(其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。

在环绕通知的方法中是需要返回一个Object类型对象的,如果把环绕通知的方法返回类型是void,将会导致一些无法预估的情况,比如:404。

Pointcut匹配 join points的谓词。Advice与切入点表达式相关联, 并在切入点匹配的任何连接点上运行。(例如,具有特定名称的方法的执行)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。

在 Spring 中, 所有的方法都可以认为是Joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配Joinpoint, 给满足规则的Joinpoint 添加 Advice。

Pointcut 和 Joinpoint在Spring AOP 中, 所有的方法执行都是 join point。 而 point cut 是一个描述信息,它修饰的是 join point, 通过 point cut,我们就可以确定哪些 join point 可以被织入Advice。 因此join point 和 point cut本质上就是两个不同维度上的东西。

advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice。

Advice概念Advice 是我们切面功能的实现,它是切点的真正执行的地方。比如像前面例子中打印时间的几个方法(被@Before等注解标注的方法都是一个通知);Advice 在 Jointpoint 处插入代码到应用程序中。

分类BeforeAdvice,AfterAdvice,区别在于Advice在目标方法之前调用还是之后调用,Throw Advice 表示当目标发生异常时调用Advice。

before advice: 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)after return advice: 在一个 join point 正常返回后执行的 adviceafter throwing advice: 当一个 join point 抛出异常后执行的 adviceafter(final) advice: 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.around advice:在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.Advice、JoinPoint、PointCut 关系下面这张图是在网上一位大佬的博客里发现的,可以帮助我们更好的理解这些概念之间的关系。

上面是对于AOP中涉及到的一些基本概念及它们之间的关系做了简单的梳理。

一些坑在调试程序过程中出现的一些问题记录

1、使用AOP拦截controller层的服务成功,但是页面报错404代码语言:javascript复制@Around("cutIn()")

public void aroundAdvice(ProceedingJoinPoint poin) {

System.out.println("环绕通知");

}

这里需要注意的是再使用环绕通知时,需要给方法一个返回值。

代码语言:javascript复制@Around("cutIn()")

public Object aroundAdvice(ProceedingJoinPoint poin) throws Throwable {

System.out.println("环绕通知");

return poin.proceed();

}

2、0 formal unbound in pointcut在spring 4.x中 提供了aop注解方式 带参数的方式。看下面例子:

代码语言:javascript复制@Pointcut(value = "execution(* com.glmapper.framerwork.service.impl.*(int,int)) && args(i,j)")

public void cutIn(int i, int j) {}

@Before(value="cutIn(i, j)",argNames = "i,j")

public void beforeMethod( int i, int j) {

System.out.println("---------begins with " + i + "-" +j);

}

比如说这里,Before中有两个int类型的参数,如果此时我们在使用时没有给其指定参数,那么就会抛出:Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut 异常信息。

本来是想放在一篇里面的,但是实在太长了,就分开吧;周末更新下

相关数据

🐉龙头纹身?酷炫还是禁忌?🤔
beat365为什么登录不了

🐉龙头纹身?酷炫还是禁忌?🤔

⌚ 10-25 👁️‍🗨️ 7895
游戏放在c盘还是d盘好?看完这篇你就明白了
365bet体育投注网

游戏放在c盘还是d盘好?看完这篇你就明白了

⌚ 08-02 👁️‍🗨️ 2623
平安怎么样
365bet体育比分直播

平安怎么样

⌚ 09-21 👁️‍🗨️ 5439