001/* 002 * Copyright 2002-2019 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.transaction.interceptor; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027 028import org.springframework.beans.factory.BeanClassLoaderAware; 029import org.springframework.beans.factory.InitializingBean; 030import org.springframework.lang.Nullable; 031import org.springframework.util.Assert; 032import org.springframework.util.ClassUtils; 033import org.springframework.util.ObjectUtils; 034import org.springframework.util.PatternMatchUtils; 035 036/** 037 * Simple {@link TransactionAttributeSource} implementation that 038 * allows attributes to be stored per method in a {@link Map}. 039 * 040 * @author Rod Johnson 041 * @author Juergen Hoeller 042 * @since 24.04.2003 043 * @see #isMatch 044 * @see NameMatchTransactionAttributeSource 045 */ 046public class MethodMapTransactionAttributeSource 047 implements TransactionAttributeSource, BeanClassLoaderAware, InitializingBean { 048 049 /** Logger available to subclasses. */ 050 protected final Log logger = LogFactory.getLog(getClass()); 051 052 /** Map from method name to attribute value. */ 053 @Nullable 054 private Map<String, TransactionAttribute> methodMap; 055 056 @Nullable 057 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 058 059 private boolean eagerlyInitialized = false; 060 061 private boolean initialized = false; 062 063 /** Map from Method to TransactionAttribute. */ 064 private final Map<Method, TransactionAttribute> transactionAttributeMap = new HashMap<>(); 065 066 /** Map from Method to name pattern used for registration. */ 067 private final Map<Method, String> methodNameMap = new HashMap<>(); 068 069 070 /** 071 * Set a name/attribute map, consisting of "FQCN.method" method names 072 * (e.g. "com.mycompany.mycode.MyClass.myMethod") and 073 * {@link TransactionAttribute} instances (or Strings to be converted 074 * to {@code TransactionAttribute} instances). 075 * <p>Intended for configuration via setter injection, typically within 076 * a Spring bean factory. Relies on {@link #afterPropertiesSet()} 077 * being called afterwards. 078 * @param methodMap said {@link Map} from method name to attribute value 079 * @see TransactionAttribute 080 * @see TransactionAttributeEditor 081 */ 082 public void setMethodMap(Map<String, TransactionAttribute> methodMap) { 083 this.methodMap = methodMap; 084 } 085 086 @Override 087 public void setBeanClassLoader(ClassLoader beanClassLoader) { 088 this.beanClassLoader = beanClassLoader; 089 } 090 091 092 /** 093 * Eagerly initializes the specified 094 * {@link #setMethodMap(java.util.Map) "methodMap"}, if any. 095 * @see #initMethodMap(java.util.Map) 096 */ 097 @Override 098 public void afterPropertiesSet() { 099 initMethodMap(this.methodMap); 100 this.eagerlyInitialized = true; 101 this.initialized = true; 102 } 103 104 /** 105 * Initialize the specified {@link #setMethodMap(java.util.Map) "methodMap"}, if any. 106 * @param methodMap a Map from method names to {@code TransactionAttribute} instances 107 * @see #setMethodMap 108 */ 109 protected void initMethodMap(@Nullable Map<String, TransactionAttribute> methodMap) { 110 if (methodMap != null) { 111 methodMap.forEach(this::addTransactionalMethod); 112 } 113 } 114 115 116 /** 117 * Add an attribute for a transactional method. 118 * <p>Method names can end or start with "*" for matching multiple methods. 119 * @param name class and method name, separated by a dot 120 * @param attr attribute associated with the method 121 * @throws IllegalArgumentException in case of an invalid name 122 */ 123 public void addTransactionalMethod(String name, TransactionAttribute attr) { 124 Assert.notNull(name, "Name must not be null"); 125 int lastDotIndex = name.lastIndexOf('.'); 126 if (lastDotIndex == -1) { 127 throw new IllegalArgumentException("'" + name + "' is not a valid method name: format is FQN.methodName"); 128 } 129 String className = name.substring(0, lastDotIndex); 130 String methodName = name.substring(lastDotIndex + 1); 131 Class<?> clazz = ClassUtils.resolveClassName(className, this.beanClassLoader); 132 addTransactionalMethod(clazz, methodName, attr); 133 } 134 135 /** 136 * Add an attribute for a transactional method. 137 * Method names can end or start with "*" for matching multiple methods. 138 * @param clazz target interface or class 139 * @param mappedName mapped method name 140 * @param attr attribute associated with the method 141 */ 142 public void addTransactionalMethod(Class<?> clazz, String mappedName, TransactionAttribute attr) { 143 Assert.notNull(clazz, "Class must not be null"); 144 Assert.notNull(mappedName, "Mapped name must not be null"); 145 String name = clazz.getName() + '.' + mappedName; 146 147 Method[] methods = clazz.getDeclaredMethods(); 148 List<Method> matchingMethods = new ArrayList<>(); 149 for (Method method : methods) { 150 if (isMatch(method.getName(), mappedName)) { 151 matchingMethods.add(method); 152 } 153 } 154 if (matchingMethods.isEmpty()) { 155 throw new IllegalArgumentException( 156 "Could not find method '" + mappedName + "' on class [" + clazz.getName() + "]"); 157 } 158 159 // Register all matching methods 160 for (Method method : matchingMethods) { 161 String regMethodName = this.methodNameMap.get(method); 162 if (regMethodName == null || (!regMethodName.equals(name) && regMethodName.length() <= name.length())) { 163 // No already registered method name, or more specific 164 // method name specification now -> (re-)register method. 165 if (logger.isDebugEnabled() && regMethodName != null) { 166 logger.debug("Replacing attribute for transactional method [" + method + "]: current name '" + 167 name + "' is more specific than '" + regMethodName + "'"); 168 } 169 this.methodNameMap.put(method, name); 170 addTransactionalMethod(method, attr); 171 } 172 else { 173 if (logger.isDebugEnabled()) { 174 logger.debug("Keeping attribute for transactional method [" + method + "]: current name '" + 175 name + "' is not more specific than '" + regMethodName + "'"); 176 } 177 } 178 } 179 } 180 181 /** 182 * Add an attribute for a transactional method. 183 * @param method the method 184 * @param attr attribute associated with the method 185 */ 186 public void addTransactionalMethod(Method method, TransactionAttribute attr) { 187 Assert.notNull(method, "Method must not be null"); 188 Assert.notNull(attr, "TransactionAttribute must not be null"); 189 if (logger.isDebugEnabled()) { 190 logger.debug("Adding transactional method [" + method + "] with attribute [" + attr + "]"); 191 } 192 this.transactionAttributeMap.put(method, attr); 193 } 194 195 /** 196 * Return if the given method name matches the mapped name. 197 * <p>The default implementation checks for "xxx*", "*xxx" and "*xxx*" 198 * matches, as well as direct equality. 199 * @param methodName the method name of the class 200 * @param mappedName the name in the descriptor 201 * @return if the names match 202 * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) 203 */ 204 protected boolean isMatch(String methodName, String mappedName) { 205 return PatternMatchUtils.simpleMatch(mappedName, methodName); 206 } 207 208 209 @Override 210 @Nullable 211 public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) { 212 if (this.eagerlyInitialized) { 213 return this.transactionAttributeMap.get(method); 214 } 215 else { 216 synchronized (this.transactionAttributeMap) { 217 if (!this.initialized) { 218 initMethodMap(this.methodMap); 219 this.initialized = true; 220 } 221 return this.transactionAttributeMap.get(method); 222 } 223 } 224 } 225 226 227 @Override 228 public boolean equals(@Nullable Object other) { 229 if (this == other) { 230 return true; 231 } 232 if (!(other instanceof MethodMapTransactionAttributeSource)) { 233 return false; 234 } 235 MethodMapTransactionAttributeSource otherTas = (MethodMapTransactionAttributeSource) other; 236 return ObjectUtils.nullSafeEquals(this.methodMap, otherTas.methodMap); 237 } 238 239 @Override 240 public int hashCode() { 241 return MethodMapTransactionAttributeSource.class.hashCode(); 242 } 243 244 @Override 245 public String toString() { 246 return getClass().getName() + ": " + this.methodMap; 247 } 248 249}