This repository was archived by the owner on Feb 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Dropwizard unit of work #50
Open
isopropylcyanide
wants to merge
4
commits into
dropwizard:master
Choose a base branch
from
isopropylcyanide:dropwizard-unit-of-work
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
src/main/java/io/dropwizard/jdbi/unitofwork/JdbiUnitOfWork.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package io.dropwizard.jdbi.unitofwork; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
|
||
import static java.lang.annotation.ElementType.METHOD; | ||
import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
||
/** | ||
* When annotating a Jersey resource method, wraps the method in a Jdbi transaction context | ||
* associated with a valid handle. | ||
* <br><br> | ||
* A transaction will automatically {@code begin} before the resource method is invoked, | ||
* {@code commit} if the method returned without throwing any exception and {@code rollback} | ||
* if an exception was thrown. | ||
*/ | ||
@Target(METHOD) | ||
@Retention(RUNTIME) | ||
@Documented | ||
public @interface JdbiUnitOfWork { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
## @JdbiUnitOfWork - Unit of Work Support | ||
|
||
Provides a `Unit of Work` annotation for a Jdbi backed Dropwizard service for wrapping resource methods in a transaction | ||
context | ||
|
||
- [`Dropwizard`](https://github.com/dropwizard/dropwizard) provides a very | ||
slick [`@UnitOfWork`](https://www.dropwizard.io/en/latest/manual/hibernate.html) annotation that wraps a transaction | ||
context around resource methods annotated with this annotation. This is very useful for wrapping multiple calls in a | ||
single database transaction all of which will succeed or roll back atomically. | ||
|
||
|
||
- However this support is only available for `Hibernate`. This module provides support for a `Jdbi`backend | ||
|
||
## Features | ||
|
||
- `transactionality` across multiple datasources when called from a request thread | ||
- `transactionality` across multiple datasources across `multiple threads` | ||
- `excluding` selectively, certain set of URI's from transaction contexts, such as `ELB`, `Health Checks` etc | ||
- `Http GET` methods are excluded from transaction by default. | ||
- `Http POST` methods are wrapped around in a transaction only when annotated with `@JdbiUnitOfWork` | ||
|
||
## Usage | ||
|
||
- Add the dependency to your `pom.xml` | ||
|
||
- Construct a `JdbiUnitOfWorkProvider` from the DBI instance. | ||
|
||
```java | ||
JdbiUnitOfWorkProvider provider = JdbiUnitOfWorkProvider.withDefault(dbi); // most common | ||
or | ||
JdbiUnitOfWorkProvider provider = JdbiUnitOfWorkProvider.withLinked(dbi); // most common | ||
``` | ||
|
||
If you are using Guice, you can bind the instance | ||
``` | ||
bind(JdbiUnitOfWorkProvider.class).toInstance(provider); | ||
``` | ||
|
||
<br> | ||
|
||
- Provide the list of package where the SQL Objects / DAO (to be attached) are located. Classes with Jdbi | ||
annotations `@SqlQuery` or `@SqlUpdate` or `@SqlBatch` or `@SqlCall` will be picked automatically. | ||
|
||
<br> | ||
|
||
Use `JdbiUnitOfWorkProvider` to generate the proxies. You can also register the classes one by one. | ||
|
||
```java | ||
|
||
// class level | ||
SampleDao dao = (SampleDao) provider.getWrappedInstanceForDaoClass(SampleDao.class); | ||
// use the proxies and pass it as they were normal instances | ||
resource = new SampleResource(dao); | ||
|
||
// package level | ||
List<String> daoPackages = Lists.newArrayList("<fq-package-name>", "fq-package-name-2", ...); | ||
Map<? extends Class, Object> proxies = unitOfWorkProvider.getWrappedInstanceForDaoPackage(daoPackages); | ||
// use the proxies and pass it as they were normal instances | ||
resource = ...new SampleResource((SampleDao)proxies.get(SampleDao.class)) | ||
``` | ||
|
||
<br> | ||
|
||
- Finally, we need to register the event listener with the Jersey Environment using the constructed provider | ||
``` | ||
environment.jersey().register(new JdbiUnitOfWorkApplicationEventListener(provider, new HashSet<>()));; | ||
``` | ||
In case you'd like to exclude certain URI paths from being monitored, you can pass them into exclude paths; | ||
``` | ||
Set<String> excludePaths = new HashSet<>(); | ||
environment.jersey().register(new JdbiUnitOfWorkApplicationEventListener(handleManager, excludePaths)); | ||
``` | ||
|
||
<br> | ||
|
||
- Start annotating resource methods with `@JdbiUnitOfWork` and you're good to go. | ||
```java | ||
@POST | ||
@Path("/") | ||
@JdbiUnitOfWork | ||
public RequestResponse createRequest() { | ||
..do stateful work (across multiple Dao's) | ||
return response | ||
} | ||
``` |
39 changes: 39 additions & 0 deletions
39
src/main/java/io/dropwizard/jdbi/unitofwork/core/DefaultJdbiHandleManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package io.dropwizard.jdbi.unitofwork.core; | ||
|
||
import org.skife.jdbi.v2.DBI; | ||
import org.skife.jdbi.v2.Handle; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* This implementation gets a new handle each time it is invoked. It simulates the default | ||
* behaviour of creating new handles each time the dao method is invoked. | ||
* <br><br> | ||
* It can be used to service requests which interact with only a single method in a single handle. | ||
* This is a lightweight implementation suitable for testing, such as with embedded databases. | ||
* Any serious application should not be using this as it may quickly leak / run out of handles | ||
* | ||
* @apiNote Not suitable for requests spanning multiple Dbi as the handle returned is different | ||
* This implementation, therefore, does not support thread factory creation. | ||
*/ | ||
public class DefaultJdbiHandleManager implements JdbiHandleManager { | ||
|
||
private final Logger log = LoggerFactory.getLogger(DefaultJdbiHandleManager.class); | ||
private final DBI dbi; | ||
|
||
public DefaultJdbiHandleManager(DBI dbi) { | ||
this.dbi = dbi; | ||
} | ||
|
||
@Override | ||
public Handle get() { | ||
Handle handle = dbi.open(); | ||
log.debug("handle [{}] : Thread Id [{}]", handle.hashCode(), Thread.currentThread().getId()); | ||
return handle; | ||
} | ||
|
||
@Override | ||
public void clear() { | ||
log.debug("No Op"); | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiHandleManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package io.dropwizard.jdbi.unitofwork.core; | ||
|
||
import org.skife.jdbi.v2.Handle; | ||
|
||
import java.util.concurrent.ThreadFactory; | ||
|
||
/** | ||
* A {@link JdbiHandleManager} is used to provide the lifecycle of a {@link Handle} with respect | ||
* to a given scope. A scope may be session based, request based or may be invoked on every run. | ||
*/ | ||
public interface JdbiHandleManager { | ||
|
||
/** | ||
* Provide a way to get a Jdbi handle, a wrapped connection to the underlying database | ||
* | ||
* @return a valid handle tied with a specific scope | ||
*/ | ||
Handle get(); | ||
|
||
/** | ||
* Provide a way to clear the handle rendering it useless for the other methods | ||
*/ | ||
void clear(); | ||
|
||
/** | ||
* Provide a thread factory for the caller with some identity represented by the | ||
* {@link #getConversationId()}. This can be used by the caller to create multiple threads, | ||
* say, using {@link java.util.concurrent.ExecutorService}. The {@link JdbiHandleManager} can | ||
* then use the thread factory to identify and manage handle use across multiple threads. | ||
* | ||
* @return a thread factory used to safely create multiple threads | ||
* @throws UnsupportedOperationException by default. Implementations overriding this method | ||
* must ensure that the conversation id is unique | ||
*/ | ||
default ThreadFactory createThreadFactory() { | ||
throw new UnsupportedOperationException("Thread factory creation is not supported"); | ||
} | ||
|
||
/** | ||
* Provide a unique identifier for the conversation with a handle. No two identifiers | ||
* should co exist at once during the application lifecycle or else handle corruption | ||
* or misuse might occur. | ||
* <br><br> | ||
* This can be relied upon by the {@link #createThreadFactory()} to reuse handles across | ||
* multiple threads spawned off a request thread. | ||
* | ||
* @return a unique identifier applicable to a scope | ||
* @implNote hashcode can not be relied upon for providing a unique identifier due to the | ||
* possibility of collision. Instead opt for a monotonically increasing counter, such as | ||
* the thread id. | ||
*/ | ||
default String getConversationId() { | ||
return String.valueOf(Thread.currentThread().getId()); | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
src/main/java/io/dropwizard/jdbi/unitofwork/core/JdbiUnitOfWorkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package io.dropwizard.jdbi.unitofwork.core; | ||
|
||
import com.google.common.collect.Sets; | ||
import com.google.common.reflect.Reflection; | ||
import org.reflections.Reflections; | ||
import org.reflections.scanners.Scanners; | ||
import org.skife.jdbi.v2.DBI; | ||
import org.skife.jdbi.v2.sqlobject.SqlBatch; | ||
import org.skife.jdbi.v2.sqlobject.SqlCall; | ||
import org.skife.jdbi.v2.sqlobject.SqlQuery; | ||
import org.skife.jdbi.v2.sqlobject.SqlUpdate; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.lang.reflect.Method; | ||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
@SuppressWarnings({"UnstableApiUsage", "rawtypes", "unchecked"}) | ||
public class JdbiUnitOfWorkProvider { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only point of access for the users unless they want to do something low level for which a getter is exposed |
||
|
||
private final Logger log = LoggerFactory.getLogger(JdbiUnitOfWorkProvider.class); | ||
private final JdbiHandleManager handleManager; | ||
|
||
private JdbiUnitOfWorkProvider(JdbiHandleManager handleManager) { | ||
this.handleManager = handleManager; | ||
} | ||
|
||
public static JdbiUnitOfWorkProvider withDefault(DBI dbi) { | ||
JdbiHandleManager handleManager = new RequestScopedJdbiHandleManager(dbi); | ||
return new JdbiUnitOfWorkProvider(handleManager); | ||
} | ||
|
||
public static JdbiUnitOfWorkProvider withLinked(DBI dbi) { | ||
JdbiHandleManager handleManager = new LinkedRequestScopedJdbiHandleManager(dbi); | ||
return new JdbiUnitOfWorkProvider(handleManager); | ||
} | ||
|
||
public JdbiHandleManager getHandleManager() { | ||
return handleManager; | ||
} | ||
|
||
/** | ||
* getWrappedInstanceForDaoClass generates a proxy instance of the dao class for which | ||
* the jdbi unit of work aspect would be wrapped around with. | ||
* <p> | ||
* Note: It is recommended to use {@link JdbiUnitOfWorkProvider#getWrappedInstanceForDaoPackage(List)} instead | ||
* as passing a list of packages is easier than passing each instance individually. | ||
* <p> | ||
* This method however may be used in case the classpath scanning is disabled. | ||
* If the original class is null or contains no relevant JDBI annotations, this method throws an | ||
* exception | ||
* | ||
* @param daoClass the DAO class for which a proxy needs to be created fo | ||
* @return the wrapped instance ready to be passed around | ||
*/ | ||
public Object getWrappedInstanceForDaoClass(Class daoClass) { | ||
if (daoClass == null) { | ||
throw new IllegalArgumentException("DAO Class cannot be null"); | ||
} | ||
boolean atLeastOneJdbiMethod = false; | ||
for (Method method : daoClass.getDeclaredMethods()) { | ||
if (method.getDeclaringClass() == daoClass) { | ||
atLeastOneJdbiMethod = method.getAnnotation(SqlQuery.class) != null; | ||
atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlUpdate.class) != null; | ||
atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlUpdate.class) != null; | ||
atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlBatch.class) != null; | ||
atLeastOneJdbiMethod = atLeastOneJdbiMethod || method.getAnnotation(SqlCall.class) != null; | ||
} | ||
} | ||
if (!atLeastOneJdbiMethod) { | ||
throw new IllegalArgumentException(String.format("Class [%s] has no method annotated with a Jdbi SQL Object", daoClass.getSimpleName())); | ||
} | ||
|
||
log.info("Binding class [{}] with proxy handler [{}] ", daoClass.getSimpleName(), handleManager.getClass().getSimpleName()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the app starts and the users have configured a provider, it looks like
|
||
ManagedHandleInvocationHandler handler = new ManagedHandleInvocationHandler<>(handleManager, daoClass); | ||
Object proxiedInstance = Reflection.newProxy(daoClass, handler); | ||
return daoClass.cast(proxiedInstance); | ||
} | ||
|
||
/** | ||
* getWrappedInstanceForDaoPackage generates a map where every DAO class identified | ||
* through the given list of packages is mapped to its initialised proxy instance | ||
* the jdbi unit of work aspect would be wrapped around with. | ||
* <p> | ||
* In case classpath scanning is disabled, use {@link JdbiUnitOfWorkProvider#getWrappedInstanceForDaoClass(Class)} | ||
* <p> | ||
* If the original package list is null, this method throws an exception | ||
* | ||
* @param daoPackages the list of packages that contain the DAO classes | ||
* @return the map mapping dao classes to its initialised proxies | ||
*/ | ||
public Map<? extends Class, Object> getWrappedInstanceForDaoPackage(List<String> daoPackages) { | ||
if (daoPackages == null) { | ||
throw new IllegalArgumentException("DAO Class package list cannot be null"); | ||
} | ||
|
||
Set<? extends Class<?>> allDaoClasses = daoPackages.stream() | ||
.map(this::getDaoClassesForPackage) | ||
.flatMap(Collection::stream) | ||
.collect(Collectors.toSet()); | ||
|
||
Map<Class, Object> classInstanceMap = new HashMap<>(); | ||
for (Class klass : allDaoClasses) { | ||
log.info("Binding class [{}] with proxy handler [{}] ", klass.getSimpleName(), handleManager.getClass().getSimpleName()); | ||
Object instance = getWrappedInstanceForDaoClass(klass); | ||
classInstanceMap.put(klass, instance); | ||
} | ||
return classInstanceMap; | ||
} | ||
|
||
private Set<? extends Class<?>> getDaoClassesForPackage(String pkg) { | ||
Set<Method> daoClasses = new HashSet<>(); | ||
|
||
Sets.SetView<Method> union = Sets.union(daoClasses, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlQuery.class)); | ||
union = Sets.union(union, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlUpdate.class)); | ||
union = Sets.union(union, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlBatch.class)); | ||
union = Sets.union(union, new Reflections(pkg, Scanners.MethodsAnnotated).getMethodsAnnotatedWith(SqlCall.class)); | ||
|
||
return union.stream() | ||
.map(Method::getDeclaringClass) | ||
.collect(Collectors.toSet()); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need
reflections
for helping users take in a list of packages and automagically return all "Dao" specific classes annotated withSqlUpdate
/SqlQuery
/SqlCall
/ SqlBatch` etc.