001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.aop.aspectj; 018 019import java.lang.annotation.Annotation; 020import java.lang.reflect.Constructor; 021import java.lang.reflect.Method; 022import java.util.ArrayList; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Set; 026 027import org.aspectj.lang.JoinPoint; 028import org.aspectj.lang.ProceedingJoinPoint; 029import org.aspectj.weaver.tools.PointcutParser; 030import org.aspectj.weaver.tools.PointcutPrimitive; 031 032import org.springframework.core.ParameterNameDiscoverer; 033import org.springframework.lang.Nullable; 034import org.springframework.util.StringUtils; 035 036/** 037 * {@link ParameterNameDiscoverer} implementation that tries to deduce parameter names 038 * for an advice method from the pointcut expression, returning, and throwing clauses. 039 * If an unambiguous interpretation is not available, it returns {@code null}. 040 * 041 * <p>This class interprets arguments in the following way: 042 * <ol> 043 * <li>If the first parameter of the method is of type {@link JoinPoint} 044 * or {@link ProceedingJoinPoint}, it is assumed to be for passing 045 * {@code thisJoinPoint} to the advice, and the parameter name will 046 * be assigned the value {@code "thisJoinPoint"}.</li> 047 * <li>If the first parameter of the method is of type 048 * {@code JoinPoint.StaticPart}, it is assumed to be for passing 049 * {@code "thisJoinPointStaticPart"} to the advice, and the parameter name 050 * will be assigned the value {@code "thisJoinPointStaticPart"}.</li> 051 * <li>If a {@link #setThrowingName(String) throwingName} has been set, and 052 * there are no unbound arguments of type {@code Throwable+}, then an 053 * {@link IllegalArgumentException} is raised. If there is more than one 054 * unbound argument of type {@code Throwable+}, then an 055 * {@link AmbiguousBindingException} is raised. If there is exactly one 056 * unbound argument of type {@code Throwable+}, then the corresponding 057 * parameter name is assigned the value <throwingName>.</li> 058 * <li>If there remain unbound arguments, then the pointcut expression is 059 * examined. Let {@code a} be the number of annotation-based pointcut 060 * expressions (@annotation, @this, @target, @args, 061 * @within, @withincode) that are used in binding form. Usage in 062 * binding form has itself to be deduced: if the expression inside the 063 * pointcut is a single string literal that meets Java variable name 064 * conventions it is assumed to be a variable name. If {@code a} is 065 * zero we proceed to the next stage. If {@code a} > 1 then an 066 * {@code AmbiguousBindingException} is raised. If {@code a} == 1, 067 * and there are no unbound arguments of type {@code Annotation+}, 068 * then an {@code IllegalArgumentException} is raised. if there is 069 * exactly one such argument, then the corresponding parameter name is 070 * assigned the value from the pointcut expression.</li> 071 * <li>If a returningName has been set, and there are no unbound arguments 072 * then an {@code IllegalArgumentException} is raised. If there is 073 * more than one unbound argument then an 074 * {@code AmbiguousBindingException} is raised. If there is exactly 075 * one unbound argument then the corresponding parameter name is assigned 076 * the value <returningName>.</li> 077 * <li>If there remain unbound arguments, then the pointcut expression is 078 * examined once more for {@code this}, {@code target}, and 079 * {@code args} pointcut expressions used in the binding form (binding 080 * forms are deduced as described for the annotation based pointcuts). If 081 * there remains more than one unbound argument of a primitive type (which 082 * can only be bound in {@code args}) then an 083 * {@code AmbiguousBindingException} is raised. If there is exactly 084 * one argument of a primitive type, then if exactly one {@code args} 085 * bound variable was found, we assign the corresponding parameter name 086 * the variable name. If there were no {@code args} bound variables 087 * found an {@code IllegalStateException} is raised. If there are 088 * multiple {@code args} bound variables, an 089 * {@code AmbiguousBindingException} is raised. At this point, if 090 * there remains more than one unbound argument we raise an 091 * {@code AmbiguousBindingException}. If there are no unbound arguments 092 * remaining, we are done. If there is exactly one unbound argument 093 * remaining, and only one candidate variable name unbound from 094 * {@code this}, {@code target}, or {@code args}, it is 095 * assigned as the corresponding parameter name. If there are multiple 096 * possibilities, an {@code AmbiguousBindingException} is raised.</li> 097 * </ol> 098 * 099 * <p>The behavior on raising an {@code IllegalArgumentException} or 100 * {@code AmbiguousBindingException} is configurable to allow this discoverer 101 * to be used as part of a chain-of-responsibility. By default the condition will 102 * be logged and the {@code getParameterNames(..)} method will simply return 103 * {@code null}. If the {@link #setRaiseExceptions(boolean) raiseExceptions} 104 * property is set to {@code true}, the conditions will be thrown as 105 * {@code IllegalArgumentException} and {@code AmbiguousBindingException}, 106 * respectively. 107 * 108 * <p>Was that perfectly clear? ;) 109 * 110 * <p>Short version: If an unambiguous binding can be deduced, then it is. 111 * If the advice requirements cannot possibly be satisfied, then {@code null} 112 * is returned. By setting the {@link #setRaiseExceptions(boolean) raiseExceptions} 113 * property to {@code true}, descriptive exceptions will be thrown instead of 114 * returning {@code null} in the case that the parameter names cannot be discovered. 115 * 116 * @author Adrian Colyer 117 * @author Juergen Hoeller 118 * @since 2.0 119 */ 120public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscoverer { 121 122 private static final String THIS_JOIN_POINT = "thisJoinPoint"; 123 private static final String THIS_JOIN_POINT_STATIC_PART = "thisJoinPointStaticPart"; 124 125 // Steps in the binding algorithm... 126 private static final int STEP_JOIN_POINT_BINDING = 1; 127 private static final int STEP_THROWING_BINDING = 2; 128 private static final int STEP_ANNOTATION_BINDING = 3; 129 private static final int STEP_RETURNING_BINDING = 4; 130 private static final int STEP_PRIMITIVE_ARGS_BINDING = 5; 131 private static final int STEP_THIS_TARGET_ARGS_BINDING = 6; 132 private static final int STEP_REFERENCE_PCUT_BINDING = 7; 133 private static final int STEP_FINISHED = 8; 134 135 private static final Set<String> singleValuedAnnotationPcds = new HashSet<>(); 136 private static final Set<String> nonReferencePointcutTokens = new HashSet<>(); 137 138 139 static { 140 singleValuedAnnotationPcds.add("@this"); 141 singleValuedAnnotationPcds.add("@target"); 142 singleValuedAnnotationPcds.add("@within"); 143 singleValuedAnnotationPcds.add("@withincode"); 144 singleValuedAnnotationPcds.add("@annotation"); 145 146 Set<PointcutPrimitive> pointcutPrimitives = PointcutParser.getAllSupportedPointcutPrimitives(); 147 for (PointcutPrimitive primitive : pointcutPrimitives) { 148 nonReferencePointcutTokens.add(primitive.getName()); 149 } 150 nonReferencePointcutTokens.add("&&"); 151 nonReferencePointcutTokens.add("!"); 152 nonReferencePointcutTokens.add("||"); 153 nonReferencePointcutTokens.add("and"); 154 nonReferencePointcutTokens.add("or"); 155 nonReferencePointcutTokens.add("not"); 156 } 157 158 159 /** The pointcut expression associated with the advice, as a simple String. */ 160 @Nullable 161 private String pointcutExpression; 162 163 private boolean raiseExceptions; 164 165 /** If the advice is afterReturning, and binds the return value, this is the parameter name used. */ 166 @Nullable 167 private String returningName; 168 169 /** If the advice is afterThrowing, and binds the thrown value, this is the parameter name used. */ 170 @Nullable 171 private String throwingName; 172 173 private Class<?>[] argumentTypes = new Class<?>[0]; 174 175 private String[] parameterNameBindings = new String[0]; 176 177 private int numberOfRemainingUnboundArguments; 178 179 180 /** 181 * Create a new discoverer that attempts to discover parameter names. 182 * from the given pointcut expression. 183 */ 184 public AspectJAdviceParameterNameDiscoverer(@Nullable String pointcutExpression) { 185 this.pointcutExpression = pointcutExpression; 186 } 187 188 189 /** 190 * Indicate whether {@link IllegalArgumentException} and {@link AmbiguousBindingException} 191 * must be thrown as appropriate in the case of failing to deduce advice parameter names. 192 * @param raiseExceptions {@code true} if exceptions are to be thrown 193 */ 194 public void setRaiseExceptions(boolean raiseExceptions) { 195 this.raiseExceptions = raiseExceptions; 196 } 197 198 /** 199 * If {@code afterReturning} advice binds the return value, the 200 * returning variable name must be specified. 201 * @param returningName the name of the returning variable 202 */ 203 public void setReturningName(@Nullable String returningName) { 204 this.returningName = returningName; 205 } 206 207 /** 208 * If {@code afterThrowing} advice binds the thrown value, the 209 * throwing variable name must be specified. 210 * @param throwingName the name of the throwing variable 211 */ 212 public void setThrowingName(@Nullable String throwingName) { 213 this.throwingName = throwingName; 214 } 215 216 217 /** 218 * Deduce the parameter names for an advice method. 219 * <p>See the {@link AspectJAdviceParameterNameDiscoverer class level javadoc} 220 * for this class for details of the algorithm used. 221 * @param method the target {@link Method} 222 * @return the parameter names 223 */ 224 @Override 225 @Nullable 226 public String[] getParameterNames(Method method) { 227 this.argumentTypes = method.getParameterTypes(); 228 this.numberOfRemainingUnboundArguments = this.argumentTypes.length; 229 this.parameterNameBindings = new String[this.numberOfRemainingUnboundArguments]; 230 231 int minimumNumberUnboundArgs = 0; 232 if (this.returningName != null) { 233 minimumNumberUnboundArgs++; 234 } 235 if (this.throwingName != null) { 236 minimumNumberUnboundArgs++; 237 } 238 if (this.numberOfRemainingUnboundArguments < minimumNumberUnboundArgs) { 239 throw new IllegalStateException( 240 "Not enough arguments in method to satisfy binding of returning and throwing variables"); 241 } 242 243 try { 244 int algorithmicStep = STEP_JOIN_POINT_BINDING; 245 while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) { 246 switch (algorithmicStep++) { 247 case STEP_JOIN_POINT_BINDING: 248 if (!maybeBindThisJoinPoint()) { 249 maybeBindThisJoinPointStaticPart(); 250 } 251 break; 252 case STEP_THROWING_BINDING: 253 maybeBindThrowingVariable(); 254 break; 255 case STEP_ANNOTATION_BINDING: 256 maybeBindAnnotationsFromPointcutExpression(); 257 break; 258 case STEP_RETURNING_BINDING: 259 maybeBindReturningVariable(); 260 break; 261 case STEP_PRIMITIVE_ARGS_BINDING: 262 maybeBindPrimitiveArgsFromPointcutExpression(); 263 break; 264 case STEP_THIS_TARGET_ARGS_BINDING: 265 maybeBindThisOrTargetOrArgsFromPointcutExpression(); 266 break; 267 case STEP_REFERENCE_PCUT_BINDING: 268 maybeBindReferencePointcutParameter(); 269 break; 270 default: 271 throw new IllegalStateException("Unknown algorithmic step: " + (algorithmicStep - 1)); 272 } 273 } 274 } 275 catch (AmbiguousBindingException | IllegalArgumentException ex) { 276 if (this.raiseExceptions) { 277 throw ex; 278 } 279 else { 280 return null; 281 } 282 } 283 284 if (this.numberOfRemainingUnboundArguments == 0) { 285 return this.parameterNameBindings; 286 } 287 else { 288 if (this.raiseExceptions) { 289 throw new IllegalStateException("Failed to bind all argument names: " + 290 this.numberOfRemainingUnboundArguments + " argument(s) could not be bound"); 291 } 292 else { 293 // convention for failing is to return null, allowing participation in a chain of responsibility 294 return null; 295 } 296 } 297 } 298 299 /** 300 * An advice method can never be a constructor in Spring. 301 * @return {@code null} 302 * @throws UnsupportedOperationException if 303 * {@link #setRaiseExceptions(boolean) raiseExceptions} has been set to {@code true} 304 */ 305 @Override 306 @Nullable 307 public String[] getParameterNames(Constructor<?> ctor) { 308 if (this.raiseExceptions) { 309 throw new UnsupportedOperationException("An advice method can never be a constructor"); 310 } 311 else { 312 // we return null rather than throw an exception so that we behave well 313 // in a chain-of-responsibility. 314 return null; 315 } 316 } 317 318 319 private void bindParameterName(int index, String name) { 320 this.parameterNameBindings[index] = name; 321 this.numberOfRemainingUnboundArguments--; 322 } 323 324 /** 325 * If the first parameter is of type JoinPoint or ProceedingJoinPoint,bind "thisJoinPoint" as 326 * parameter name and return true, else return false. 327 */ 328 private boolean maybeBindThisJoinPoint() { 329 if ((this.argumentTypes[0] == JoinPoint.class) || (this.argumentTypes[0] == ProceedingJoinPoint.class)) { 330 bindParameterName(0, THIS_JOIN_POINT); 331 return true; 332 } 333 else { 334 return false; 335 } 336 } 337 338 private void maybeBindThisJoinPointStaticPart() { 339 if (this.argumentTypes[0] == JoinPoint.StaticPart.class) { 340 bindParameterName(0, THIS_JOIN_POINT_STATIC_PART); 341 } 342 } 343 344 /** 345 * If a throwing name was specified and there is exactly one choice remaining 346 * (argument that is a subtype of Throwable) then bind it. 347 */ 348 private void maybeBindThrowingVariable() { 349 if (this.throwingName == null) { 350 return; 351 } 352 353 // So there is binding work to do... 354 int throwableIndex = -1; 355 for (int i = 0; i < this.argumentTypes.length; i++) { 356 if (isUnbound(i) && isSubtypeOf(Throwable.class, i)) { 357 if (throwableIndex == -1) { 358 throwableIndex = i; 359 } 360 else { 361 // Second candidate we've found - ambiguous binding 362 throw new AmbiguousBindingException("Binding of throwing parameter '" + 363 this.throwingName + "' is ambiguous: could be bound to argument " + 364 throwableIndex + " or argument " + i); 365 } 366 } 367 } 368 369 if (throwableIndex == -1) { 370 throw new IllegalStateException("Binding of throwing parameter '" + this.throwingName 371 + "' could not be completed as no available arguments are a subtype of Throwable"); 372 } 373 else { 374 bindParameterName(throwableIndex, this.throwingName); 375 } 376 } 377 378 /** 379 * If a returning variable was specified and there is only one choice remaining, bind it. 380 */ 381 private void maybeBindReturningVariable() { 382 if (this.numberOfRemainingUnboundArguments == 0) { 383 throw new IllegalStateException( 384 "Algorithm assumes that there must be at least one unbound parameter on entry to this method"); 385 } 386 387 if (this.returningName != null) { 388 if (this.numberOfRemainingUnboundArguments > 1) { 389 throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName + 390 "' is ambiguous, there are " + this.numberOfRemainingUnboundArguments + " candidates."); 391 } 392 393 // We're all set... find the unbound parameter, and bind it. 394 for (int i = 0; i < this.parameterNameBindings.length; i++) { 395 if (this.parameterNameBindings[i] == null) { 396 bindParameterName(i, this.returningName); 397 break; 398 } 399 } 400 } 401 } 402 403 404 /** 405 * Parse the string pointcut expression looking for: 406 * @this, @target, @args, @within, @withincode, @annotation. 407 * If we find one of these pointcut expressions, try and extract a candidate variable 408 * name (or variable names, in the case of args). 409 * <p>Some more support from AspectJ in doing this exercise would be nice... :) 410 */ 411 private void maybeBindAnnotationsFromPointcutExpression() { 412 List<String> varNames = new ArrayList<>(); 413 String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); 414 for (int i = 0; i < tokens.length; i++) { 415 String toMatch = tokens[i]; 416 int firstParenIndex = toMatch.indexOf('('); 417 if (firstParenIndex != -1) { 418 toMatch = toMatch.substring(0, firstParenIndex); 419 } 420 if (singleValuedAnnotationPcds.contains(toMatch)) { 421 PointcutBody body = getPointcutBody(tokens, i); 422 i += body.numTokensConsumed; 423 String varName = maybeExtractVariableName(body.text); 424 if (varName != null) { 425 varNames.add(varName); 426 } 427 } 428 else if (tokens[i].startsWith("@args(") || tokens[i].equals("@args")) { 429 PointcutBody body = getPointcutBody(tokens, i); 430 i += body.numTokensConsumed; 431 maybeExtractVariableNamesFromArgs(body.text, varNames); 432 } 433 } 434 435 bindAnnotationsFromVarNames(varNames); 436 } 437 438 /** 439 * Match the given list of extracted variable names to argument slots. 440 */ 441 private void bindAnnotationsFromVarNames(List<String> varNames) { 442 if (!varNames.isEmpty()) { 443 // we have work to do... 444 int numAnnotationSlots = countNumberOfUnboundAnnotationArguments(); 445 if (numAnnotationSlots > 1) { 446 throw new AmbiguousBindingException("Found " + varNames.size() + 447 " potential annotation variable(s), and " + 448 numAnnotationSlots + " potential argument slots"); 449 } 450 else if (numAnnotationSlots == 1) { 451 if (varNames.size() == 1) { 452 // it's a match 453 findAndBind(Annotation.class, varNames.get(0)); 454 } 455 else { 456 // multiple candidate vars, but only one slot 457 throw new IllegalArgumentException("Found " + varNames.size() + 458 " candidate annotation binding variables" + 459 " but only one potential argument binding slot"); 460 } 461 } 462 else { 463 // no slots so presume those candidate vars were actually type names 464 } 465 } 466 } 467 468 /* 469 * If the token starts meets Java identifier conventions, it's in. 470 */ 471 @Nullable 472 private String maybeExtractVariableName(@Nullable String candidateToken) { 473 if (!StringUtils.hasLength(candidateToken)) { 474 return null; 475 } 476 if (Character.isJavaIdentifierStart(candidateToken.charAt(0)) && 477 Character.isLowerCase(candidateToken.charAt(0))) { 478 char[] tokenChars = candidateToken.toCharArray(); 479 for (char tokenChar : tokenChars) { 480 if (!Character.isJavaIdentifierPart(tokenChar)) { 481 return null; 482 } 483 } 484 return candidateToken; 485 } 486 else { 487 return null; 488 } 489 } 490 491 /** 492 * Given an args pointcut body (could be {@code args} or {@code at_args}), 493 * add any candidate variable names to the given list. 494 */ 495 private void maybeExtractVariableNamesFromArgs(@Nullable String argsSpec, List<String> varNames) { 496 if (argsSpec == null) { 497 return; 498 } 499 String[] tokens = StringUtils.tokenizeToStringArray(argsSpec, ","); 500 for (int i = 0; i < tokens.length; i++) { 501 tokens[i] = StringUtils.trimWhitespace(tokens[i]); 502 String varName = maybeExtractVariableName(tokens[i]); 503 if (varName != null) { 504 varNames.add(varName); 505 } 506 } 507 } 508 509 /** 510 * Parse the string pointcut expression looking for this(), target() and args() expressions. 511 * If we find one, try and extract a candidate variable name and bind it. 512 */ 513 private void maybeBindThisOrTargetOrArgsFromPointcutExpression() { 514 if (this.numberOfRemainingUnboundArguments > 1) { 515 throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments 516 + " unbound args at this(),target(),args() binding stage, with no way to determine between them"); 517 } 518 519 List<String> varNames = new ArrayList<>(); 520 String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); 521 for (int i = 0; i < tokens.length; i++) { 522 if (tokens[i].equals("this") || 523 tokens[i].startsWith("this(") || 524 tokens[i].equals("target") || 525 tokens[i].startsWith("target(")) { 526 PointcutBody body = getPointcutBody(tokens, i); 527 i += body.numTokensConsumed; 528 String varName = maybeExtractVariableName(body.text); 529 if (varName != null) { 530 varNames.add(varName); 531 } 532 } 533 else if (tokens[i].equals("args") || tokens[i].startsWith("args(")) { 534 PointcutBody body = getPointcutBody(tokens, i); 535 i += body.numTokensConsumed; 536 List<String> candidateVarNames = new ArrayList<>(); 537 maybeExtractVariableNamesFromArgs(body.text, candidateVarNames); 538 // we may have found some var names that were bound in previous primitive args binding step, 539 // filter them out... 540 for (String varName : candidateVarNames) { 541 if (!alreadyBound(varName)) { 542 varNames.add(varName); 543 } 544 } 545 } 546 } 547 548 549 if (varNames.size() > 1) { 550 throw new AmbiguousBindingException("Found " + varNames.size() + 551 " candidate this(), target() or args() variables but only one unbound argument slot"); 552 } 553 else if (varNames.size() == 1) { 554 for (int j = 0; j < this.parameterNameBindings.length; j++) { 555 if (isUnbound(j)) { 556 bindParameterName(j, varNames.get(0)); 557 break; 558 } 559 } 560 } 561 // else varNames.size must be 0 and we have nothing to bind. 562 } 563 564 private void maybeBindReferencePointcutParameter() { 565 if (this.numberOfRemainingUnboundArguments > 1) { 566 throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments 567 + " unbound args at reference pointcut binding stage, with no way to determine between them"); 568 } 569 570 List<String> varNames = new ArrayList<>(); 571 String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); 572 for (int i = 0; i < tokens.length; i++) { 573 String toMatch = tokens[i]; 574 if (toMatch.startsWith("!")) { 575 toMatch = toMatch.substring(1); 576 } 577 int firstParenIndex = toMatch.indexOf('('); 578 if (firstParenIndex != -1) { 579 toMatch = toMatch.substring(0, firstParenIndex); 580 } 581 else { 582 if (tokens.length < i + 2) { 583 // no "(" and nothing following 584 continue; 585 } 586 else { 587 String nextToken = tokens[i + 1]; 588 if (nextToken.charAt(0) != '(') { 589 // next token is not "(" either, can't be a pc... 590 continue; 591 } 592 } 593 594 } 595 596 // eat the body 597 PointcutBody body = getPointcutBody(tokens, i); 598 i += body.numTokensConsumed; 599 600 if (!nonReferencePointcutTokens.contains(toMatch)) { 601 // then it could be a reference pointcut 602 String varName = maybeExtractVariableName(body.text); 603 if (varName != null) { 604 varNames.add(varName); 605 } 606 } 607 } 608 609 if (varNames.size() > 1) { 610 throw new AmbiguousBindingException("Found " + varNames.size() + 611 " candidate reference pointcut variables but only one unbound argument slot"); 612 } 613 else if (varNames.size() == 1) { 614 for (int j = 0; j < this.parameterNameBindings.length; j++) { 615 if (isUnbound(j)) { 616 bindParameterName(j, varNames.get(0)); 617 break; 618 } 619 } 620 } 621 // else varNames.size must be 0 and we have nothing to bind. 622 } 623 624 /* 625 * We've found the start of a binding pointcut at the given index into the 626 * token array. Now we need to extract the pointcut body and return it. 627 */ 628 private PointcutBody getPointcutBody(String[] tokens, int startIndex) { 629 int numTokensConsumed = 0; 630 String currentToken = tokens[startIndex]; 631 int bodyStart = currentToken.indexOf('('); 632 if (currentToken.charAt(currentToken.length() - 1) == ')') { 633 // It's an all in one... get the text between the first (and the last) 634 return new PointcutBody(0, currentToken.substring(bodyStart + 1, currentToken.length() - 1)); 635 } 636 else { 637 StringBuilder sb = new StringBuilder(); 638 if (bodyStart >= 0 && bodyStart != (currentToken.length() - 1)) { 639 sb.append(currentToken.substring(bodyStart + 1)); 640 sb.append(" "); 641 } 642 numTokensConsumed++; 643 int currentIndex = startIndex + numTokensConsumed; 644 while (currentIndex < tokens.length) { 645 if (tokens[currentIndex].equals("(")) { 646 currentIndex++; 647 continue; 648 } 649 650 if (tokens[currentIndex].endsWith(")")) { 651 sb.append(tokens[currentIndex], 0, tokens[currentIndex].length() - 1); 652 return new PointcutBody(numTokensConsumed, sb.toString().trim()); 653 } 654 655 String toAppend = tokens[currentIndex]; 656 if (toAppend.startsWith("(")) { 657 toAppend = toAppend.substring(1); 658 } 659 sb.append(toAppend); 660 sb.append(" "); 661 currentIndex++; 662 numTokensConsumed++; 663 } 664 665 } 666 667 // We looked and failed... 668 return new PointcutBody(numTokensConsumed, null); 669 } 670 671 /** 672 * Match up args against unbound arguments of primitive types. 673 */ 674 private void maybeBindPrimitiveArgsFromPointcutExpression() { 675 int numUnboundPrimitives = countNumberOfUnboundPrimitiveArguments(); 676 if (numUnboundPrimitives > 1) { 677 throw new AmbiguousBindingException("Found '" + numUnboundPrimitives + 678 "' unbound primitive arguments with no way to distinguish between them."); 679 } 680 if (numUnboundPrimitives == 1) { 681 // Look for arg variable and bind it if we find exactly one... 682 List<String> varNames = new ArrayList<>(); 683 String[] tokens = StringUtils.tokenizeToStringArray(this.pointcutExpression, " "); 684 for (int i = 0; i < tokens.length; i++) { 685 if (tokens[i].equals("args") || tokens[i].startsWith("args(")) { 686 PointcutBody body = getPointcutBody(tokens, i); 687 i += body.numTokensConsumed; 688 maybeExtractVariableNamesFromArgs(body.text, varNames); 689 } 690 } 691 if (varNames.size() > 1) { 692 throw new AmbiguousBindingException("Found " + varNames.size() + 693 " candidate variable names but only one candidate binding slot when matching primitive args"); 694 } 695 else if (varNames.size() == 1) { 696 // 1 primitive arg, and one candidate... 697 for (int i = 0; i < this.argumentTypes.length; i++) { 698 if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) { 699 bindParameterName(i, varNames.get(0)); 700 break; 701 } 702 } 703 } 704 } 705 } 706 707 /* 708 * Return true if the parameter name binding for the given parameter 709 * index has not yet been assigned. 710 */ 711 private boolean isUnbound(int i) { 712 return this.parameterNameBindings[i] == null; 713 } 714 715 private boolean alreadyBound(String varName) { 716 for (int i = 0; i < this.parameterNameBindings.length; i++) { 717 if (!isUnbound(i) && varName.equals(this.parameterNameBindings[i])) { 718 return true; 719 } 720 } 721 return false; 722 } 723 724 /* 725 * Return {@code true} if the given argument type is a subclass 726 * of the given supertype. 727 */ 728 private boolean isSubtypeOf(Class<?> supertype, int argumentNumber) { 729 return supertype.isAssignableFrom(this.argumentTypes[argumentNumber]); 730 } 731 732 private int countNumberOfUnboundAnnotationArguments() { 733 int count = 0; 734 for (int i = 0; i < this.argumentTypes.length; i++) { 735 if (isUnbound(i) && isSubtypeOf(Annotation.class, i)) { 736 count++; 737 } 738 } 739 return count; 740 } 741 742 private int countNumberOfUnboundPrimitiveArguments() { 743 int count = 0; 744 for (int i = 0; i < this.argumentTypes.length; i++) { 745 if (isUnbound(i) && this.argumentTypes[i].isPrimitive()) { 746 count++; 747 } 748 } 749 return count; 750 } 751 752 /* 753 * Find the argument index with the given type, and bind the given 754 * {@code varName} in that position. 755 */ 756 private void findAndBind(Class<?> argumentType, String varName) { 757 for (int i = 0; i < this.argumentTypes.length; i++) { 758 if (isUnbound(i) && isSubtypeOf(argumentType, i)) { 759 bindParameterName(i, varName); 760 return; 761 } 762 } 763 throw new IllegalStateException("Expected to find an unbound argument of type '" + 764 argumentType.getName() + "'"); 765 } 766 767 768 /** 769 * Simple struct to hold the extracted text from a pointcut body, together 770 * with the number of tokens consumed in extracting it. 771 */ 772 private static class PointcutBody { 773 774 private int numTokensConsumed; 775 776 @Nullable 777 private String text; 778 779 public PointcutBody(int tokens, @Nullable String text) { 780 this.numTokensConsumed = tokens; 781 this.text = text; 782 } 783 } 784 785 786 /** 787 * Thrown in response to an ambiguous binding being detected when 788 * trying to resolve a method's parameter names. 789 */ 790 @SuppressWarnings("serial") 791 public static class AmbiguousBindingException extends RuntimeException { 792 793 /** 794 * Construct a new AmbiguousBindingException with the specified message. 795 * @param msg the detail message 796 */ 797 public AmbiguousBindingException(String msg) { 798 super(msg); 799 } 800 } 801 802}