spring-projects/spring-security · error · AnnotationConfigurationException

Please ensure there is one unique annotation of type

Error message

Please ensure there is one unique annotation of type %s attributed to %s. Found %d competing annotations: %s

What it means

The unique-security-annotation scanner requires exactly one annotation of the target type on the element it inspects. When it finds zero-plus-competing (multiple) annotations of the same security type on a class or method it throws AnnotationConfigurationException with the element and list of competing annotations, because it cannot decide which annotation authorizes the method.

Solutions

  1. Remove the duplicate annotation so exactly one remains on the target element
  2. Keep security annotations on one level only (method OR class), not duplicated across both when the scanner merges them
  3. If you need multiple rules, combine them into a single expression (e.g. @PreAuthorize("hasRole('A') or hasRole('B')")) instead of multiple annotations

Example fix

// before
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAuthority('scope:read')")
public void read() {}
// after
@PreAuthorize("hasRole('ADMIN') or hasAuthority('scope:read')")
public void read() {}
Defensive patterns

Strategy: validation

Validate before calling

long count = AnnotationUtils.findRepeatableAnnotations(method, PreAuthorize.class).size(); if (count > 1) throw new IllegalStateException("duplicate security annotation");

Try / catch

try { scanner.merge(element, targetClass); } catch (AnnotationConfigurationException ex) { logger.error("Duplicate security annotations: " + ex.getMessage()); }

Prevention

When it happens

Trigger: Declaring two security annotations of the same type on one method or on both the class and an overriding method when lookup merges them — e.g. two @PreAuthorize annotations, or @PreAuthorize at class level plus a competing duplicate at method level where composition yields more than one match.

Common situations: Accidental duplicate imports causing two similar security annotations; meta-annotation composition accidentally matching the same type twice; copy-pasting @PreAuthorize from interface to implementation with an additional one already present.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/9d9061a685772919. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java:140

			return this.uniqueMethodAnnotationCache.computeIfAbsent(new MethodClassKey(method, targetClass), (k) -> {
				List<MergedAnnotation<A>> annotations = findMethodAnnotations(method, targetClass);
				return requireUnique(method, annotations);
			});
		}
		throw new AnnotationConfigurationException("Unsupported element of type " + element.getClass());
	}

	private @Nullable MergedAnnotation<A> requireUnique(AnnotatedElement element,
			List<MergedAnnotation<A>> annotations) {
		return switch (annotations.size()) {
			case 0 -> null;
			case 1 -> annotations.get(0);
			default -> {
				List<Annotation> synthesized = new ArrayList<>();
				for (MergedAnnotation<A> annotation : annotations) {
					synthesized.add(annotation.synthesize());
				}
				throw new AnnotationConfigurationException("""
						Please ensure there is one unique annotation of type %s attributed to %s. \
						Found %d competing annotations: %s""".formatted(this.types, element, annotations.size(),
						synthesized));
			}
		};
	}

	private List<MergedAnnotation<A>> findParameterAnnotations(Parameter current) {
		List<MergedAnnotation<A>> directAnnotations = findDirectAnnotations(current);
		if (!directAnnotations.isEmpty()) {
			return directAnnotations;
		}
		Executable executable = current.getDeclaringExecutable();
		if (executable instanceof Method method) {
			directAnnotations = findClosestParameterAnnotations(method, method.getDeclaringClass(), current,
					new HashSet<>());
			if (!directAnnotations.isEmpty()) {
				return directAnnotations;

View on GitHub (pinned to 96852e8860)