From 21375449db051852ec76824845e6a6af88395d9e Mon Sep 17 00:00:00 2001 From: Pim van der Loos Date: Fri, 28 May 2021 12:25:31 +0200 Subject: [PATCH] Add Locked, Locked.Read, and Locked.Write annotations - Added Locked[.Read/Write] annotations which can be applied to method to lock the method body using a Reentrant[ReadWrite]Lock. --- AUTHORS | 1 + src/core/lombok/ConfigurationKeys.java | 13 +- src/core/lombok/experimental/Locked.java | 62 ++++++ .../lombok/javac/handlers/HandleLocked.java | 46 ++++ .../javac/handlers/HandleLockedRead.java | 49 +++++ .../javac/handlers/HandleLockedUtil.java | 198 ++++++++++++++++++ .../javac/handlers/HandleLockedWrite.java | 49 +++++ 7 files changed, 416 insertions(+), 2 deletions(-) create mode 100644 src/core/lombok/experimental/Locked.java create mode 100644 src/core/lombok/javac/handlers/HandleLocked.java create mode 100644 src/core/lombok/javac/handlers/HandleLockedRead.java create mode 100644 src/core/lombok/javac/handlers/HandleLockedUtil.java create mode 100644 src/core/lombok/javac/handlers/HandleLockedWrite.java diff --git a/AUTHORS b/AUTHORS index 50f9e10e..bbbebb7c 100755 --- a/AUTHORS +++ b/AUTHORS @@ -37,6 +37,7 @@ Pascal Bihler Peter Grant Philipp Eichhorn Philippe Charles +Pim van der Loos Rabea Gransberger Raul Wißfeld Reinier Zwitserloot diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index 93bcb85f..87f26122 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -391,7 +391,7 @@ public class ConfigurationKeys { * If set, any usage of {@code @Synchronized} results in a warning / error. */ public static final ConfigurationKey SYNCHRONIZED_FLAG_USAGE = new ConfigurationKey("lombok.synchronized.flagUsage", "Emit a warning or error if @Synchronized is used.") {}; - + // ----- val ----- /** @@ -623,7 +623,16 @@ public class ConfigurationKeys { * If set, any usage of {@code @Helper} results in a warning / error. */ public static final ConfigurationKey HELPER_FLAG_USAGE = new ConfigurationKey("lombok.helper.flagUsage", "Emit a warning or error if @Helper is used.") {}; - + + // ----- LOCKED ----- + + /** + * lombok configuration: {@code lombok.locked.flagUsage} = {@code WARNING} | {@code ERROR}. + * + * If set, any usage of {@code @Locked} results in a warning / error. + */ + public static final ConfigurationKey LOCKED_FLAG_USAGE = new ConfigurationKey("lombok.locked.flagUsage", "Emit a warning or error if @Locked is used.") {}; + // ----- onX ----- /** diff --git a/src/core/lombok/experimental/Locked.java b/src/core/lombok/experimental/Locked.java new file mode 100644 index 00000000..61e59ee4 --- /dev/null +++ b/src/core/lombok/experimental/Locked.java @@ -0,0 +1,62 @@ +package lombok.experimental; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.locks.ReadWriteLock; + +/** + * Uses a {@link java.util.concurrent.locks.Lock} as a more flexible alternative to the 'synchronized' keyword with + * optional increased throughput when making use of read and write locks. + *

+ * For non-static methods, a field named {@code lock} is used, and for static methods, + * {@code LOCK} is used. These will be generated if needed and if they aren't already present. The contents + * of the fields will be serializable. + *

+ * Because {@link Locked} uses a different type of lock from {@link Locked.Read} and {@link Locked.Write}, using both in + * the same class using the default names will result in a compile time error. + *

+ * Complete documentation is found at the project lombok features page for @Locked. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface Locked { + /** + * Locks using a {@link ReadWriteLock#readLock()}. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.SOURCE) + public @interface Read { + /** + * Optional: specify the name of a different field to lock on. If this field doesn't already exist, it is generated + * automatically. + * + * @return Name of the field to lock on (blank = generate one). + */ + String value() default ""; + } + + /** + * Locks using a {@link ReadWriteLock#writeLock()}. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.SOURCE) + public @interface Write { + /** + * Optional: specify the name of a different field to lock on. If this field doesn't already exist, it is generated + * automatically. + * + * @return Name of the field to lock on (blank = generate one). + */ + String value() default ""; + } + + /** + * Optional: specify the name of a different field to lock on. If this field doesn't already exist, it is generated + * automatically. + * + * @return Name of the field to lock on (blank = generate one). + */ + String value() default ""; +} diff --git a/src/core/lombok/javac/handlers/HandleLocked.java b/src/core/lombok/javac/handlers/HandleLocked.java new file mode 100644 index 00000000..7eb1c5c9 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleLocked.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2009-2021 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.experimental.Locked; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.spi.Provides; + +import static com.sun.tools.javac.tree.JCTree.*; + +/** + * Handles the {@code lombok.Locked} annotation for javac. + */ +@Provides +@HandlerPriority(value = 1024) // 2^10; @NonNull must have run first, so that we wrap around the statements generated by it. +public class HandleLocked extends JavacAnnotationHandler { + private static final String ANNOTATION_NAME = "@Locked"; + private static final String[] LOCK_CLASS = new String[]{"java", "util", "concurrent", "locks", "ReentrantLock"}; + + @Override public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + String annotationValue = annotation.getInstance().value(); + HandleLockedUtil.handle(annotationValue, ast, annotationNode, Locked.class, ANNOTATION_NAME, LOCK_CLASS); + } +} diff --git a/src/core/lombok/javac/handlers/HandleLockedRead.java b/src/core/lombok/javac/handlers/HandleLockedRead.java new file mode 100644 index 00000000..30be1cc3 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleLockedRead.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009-2021 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.experimental.Locked; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.spi.Provides; + +import static com.sun.tools.javac.tree.JCTree.*; + +/** + * Handles the {@code lombok.Locked.Read} annotation for javac. + */ +@Provides +@HandlerPriority(value = 1024) // 2^10; @NonNull must have run first, so that we wrap around the statements generated by it. +public class HandleLockedRead extends JavacAnnotationHandler { + private static final String LOCK_METHOD = "readLock"; + private static final String ANNOTATION_NAME = "@Locked.Read"; + private static final String[] LOCK_CLASS = new String[]{"java", "util", "concurrent", "locks", + "ReentrantReadWriteLock"}; + + @Override public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + String annotationValue = annotation.getInstance().value(); + HandleLockedUtil.handle(annotationValue, ast, annotationNode, Locked.Read.class, + ANNOTATION_NAME, LOCK_CLASS, LOCK_METHOD); + } +} diff --git a/src/core/lombok/javac/handlers/HandleLockedUtil.java b/src/core/lombok/javac/handlers/HandleLockedUtil.java new file mode 100644 index 00000000..9e51eeba --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleLockedUtil.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2009-2021 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.List; +import lombok.ConfigurationKeys; +import lombok.core.AST; +import lombok.experimental.Locked; +import lombok.javac.JavacNode; +import lombok.javac.JavacTreeMaker; + +import java.lang.annotation.Annotation; + +import static com.sun.tools.javac.tree.JCTree.*; +import static lombok.core.handlers.HandlerUtil.handleFlagUsage; +import static lombok.javac.handlers.JavacHandlerUtil.*; + +/** + * Container for static utility methods used by the Locked[.Read/Write] annotations. + */ +public final class HandleLockedUtil { + private static final String INSTANCE_LOCK_NAME = "lock"; + private static final String STATIC_LOCK_NAME = "LOCK"; + private static final List NIL_EXPRESSION = List.nil(); + + private HandleLockedUtil() { + //Prevent instantiation + } + + /** + * See {@link #handle(String, JCAnnotation, JavacNode, Class, String, String[], String)} for + * {@code lockableMethodName = null}. + */ + public static void handle(String annotationValue, JCTree.JCAnnotation ast, + JavacNode annotationNode, Class annotationClass, + String annotationName, String[] lockClass) { + handle(annotationValue, ast, annotationNode, annotationClass, annotationName, lockClass, null); + } + + /** + * Called when an annotation is found that is likely to match {@link Locked}, {@link Locked.Read}, or + * {@link Locked.Write}. + * + * Be aware that you'll be called for ANY annotation node in the source that looks like a match. There is, + * for example, no guarantee that the annotation node belongs to a method, even if you set your + * TargetType in the annotation to methods only. + * + * @param annotationValue The value of the annotation. This will be the name of the object used for locking. + * @param ast The javac AST node representing the annotation. + * @param annotationNode The Lombok AST wrapper around the 'ast' parameter. You can use this object + * to travel back up the chain (something javac AST can't do) to the parent of the annotation, as well + * as access useful methods such as generating warnings or errors focused on the annotation. + * @param annotationClass The specific annotation class. This should be one of {@code Locked}, {@code Locked.Read}, + * or {@code Locked.Write}. + * @param annotationName The name of the annotation to use when referencing it in errors. + * @param lockClass The type of lock to use. + * See {@link JavacHandlerUtil#chainDots(JavacNode, String, String, String...)}. + * @param lockableMethodName The name of the method in the {@code lockClass} that returns a + * {@link java.util.concurrent.locks.Lock} object. When this is {@code null}, it is assumed that {@code lockClass} + * itself can be locked/unlocked. + * @param The annotation type. + */ + public static void handle(String annotationValue, JCTree.JCAnnotation ast, + JavacNode annotationNode, Class annotationClass, + String annotationName, String[] lockClass, + String lockableMethodName) { + handleFlagUsage(annotationNode, ConfigurationKeys.LOCKED_FLAG_USAGE, annotationName); + + if (inNetbeansEditor(annotationNode)) return; + deleteAnnotationIfNeccessary(annotationNode, annotationClass); + + JavacNode methodNode = annotationNode.up(); + if (methodNode == null || methodNode.getKind() != AST.Kind.METHOD || + !(methodNode.get() instanceof JCMethodDecl)) { + annotationNode.addError(annotationName + " is legal only on methods."); + return; + } + + JCMethodDecl method = (JCMethodDecl) methodNode.get(); + if ((method.mods.flags & Flags.ABSTRACT) != 0) { + annotationNode.addError(annotationName + " is legal only on concrete methods."); + return; + } + + JavacNode typeNode = upToTypeNode(annotationNode); + if (!isClassOrEnum(typeNode)) { + annotationNode.addError(annotationName + " is legal only on methods in classes and enums."); + return; + } + + boolean[] isStatic = new boolean[]{(method.mods.flags & Flags.STATIC) != 0}; + String lockName = annotationValue; + + if (lockName.length() == 0) { + lockName = isStatic[0] ? STATIC_LOCK_NAME : INSTANCE_LOCK_NAME; + } + + JavacTreeMaker maker = methodNode.getTreeMaker().at(ast.pos); + + MemberExistsResult exists = MemberExistsResult.NOT_EXISTS; + + JCExpression lockType = chainDots(methodNode, lockClass); + + if (typeNode != null && typeNode.get() instanceof JCClassDecl) { + for (JCTree def : ((JCClassDecl) typeNode.get()).defs) { + if (def instanceof JCVariableDecl) { + if (((JCVariableDecl) def).name.contentEquals(lockName)) { + JCVariableDecl varDeclDef = (JCVariableDecl) def; + exists = getGeneratedBy(varDeclDef) == null ? MemberExistsResult.EXISTS_BY_USER : + MemberExistsResult.EXISTS_BY_LOMBOK; + boolean st = ((varDeclDef.mods.flags) & Flags.STATIC) != 0; + + if (isStatic[0] != st && exists == MemberExistsResult.EXISTS_BY_LOMBOK) { + annotationNode.addError("The generated field " + lockName + + " does not match the static status of this method"); + return; + } + isStatic[0] = st; + + if (exists == MemberExistsResult.EXISTS_BY_LOMBOK && + !lockType.toString().equals(varDeclDef.vartype.toString())) { + annotationNode.addError("Expected field " + lockName + " to be of type " + lockType + + " but got type " + varDeclDef.vartype + "!" + + " Did you mix @Locked with @Locked.Read/Write on the same generated field?"); + return; + } + } + } + } + } + + if (exists == MemberExistsResult.NOT_EXISTS) { + JCNewClass lockInstance = maker.NewClass(null, NIL_EXPRESSION, lockType, NIL_EXPRESSION, null); + JCVariableDecl newLockField = recursiveSetGeneratedBy(maker.VarDef( + maker.Modifiers(Flags.PRIVATE | Flags.FINAL | (isStatic[0] ? Flags.STATIC : 0)), + methodNode.toName(lockName), lockType, lockInstance + ), annotationNode); + + injectFieldAndMarkGenerated(methodNode.up(), newLockField); + } + + if (method.body == null || typeNode == null) return; + + JCExpression lockNode; + if (isStatic[0]) { + lockNode = namePlusTypeParamsToTypeReference(maker, typeNode, methodNode.toName(lockName), false, + List.nil()); + } else { + lockNode = maker.Select(maker.Ident(methodNode.toName("this")), methodNode.toName(lockName)); + } + + recursiveSetGeneratedBy(lockNode, annotationNode); + + final JCExpression lockable; + if (lockableMethodName == null) { + lockable = lockNode; + } else { + lockable = maker.Apply(NIL_EXPRESSION, + maker.Select(lockNode, methodNode.toName(lockableMethodName)), + NIL_EXPRESSION); + } + + JCExpressionStatement acquireLock = + maker.Exec(maker.Apply(NIL_EXPRESSION, maker.Select(lockable, typeNode.toName("lock")), NIL_EXPRESSION)); + JCExpressionStatement releaseLock = + maker.Exec(maker.Apply(NIL_EXPRESSION, maker.Select(lockable, typeNode.toName("unlock")), NIL_EXPRESSION)); + + JCTry tryBlock = recursiveSetGeneratedBy(maker.Try(maker.Block(0L, method.body.stats), + List.nil(), + maker.Block(0L, List.of(releaseLock))), + annotationNode); + + method.body.stats = List.of(tryBlock) + .prepend(recursiveSetGeneratedBy(acquireLock, annotationNode)); + methodNode.rebuild(); + } +} diff --git a/src/core/lombok/javac/handlers/HandleLockedWrite.java b/src/core/lombok/javac/handlers/HandleLockedWrite.java new file mode 100644 index 00000000..cbcb2f32 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleLockedWrite.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009-2021 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.experimental.Locked; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.spi.Provides; + +import static com.sun.tools.javac.tree.JCTree.*; + +/** + * Handles the {@code lombok.Locked.Write} annotation for javac. + */ +@Provides +@HandlerPriority(value = 1024) // 2^10; @NonNull must have run first, so that we wrap around the statements generated by it. +public class HandleLockedWrite extends JavacAnnotationHandler { + private static final String LOCK_METHOD = "writeLock"; + private static final String ANNOTATION_NAME = "@Locked.Write"; + private static final String[] LOCK_CLASS = new String[]{"java", "util", "concurrent", "locks", + "ReentrantReadWriteLock"}; + + @Override public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + String annotationValue = annotation.getInstance().value(); + HandleLockedUtil.handle(annotationValue, ast, annotationNode, Locked.Write.class, + ANNOTATION_NAME, LOCK_CLASS, LOCK_METHOD); + } +}