Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring Jdbi repositories #2528

Merged
merged 3 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 11 additions & 3 deletions spring5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-core</artifactId>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-sqlobject</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
Expand All @@ -44,6 +48,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
Expand All @@ -53,13 +61,13 @@
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-testing</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.spring5;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

/**
* Annotating a spring configuration class with this annotation enables the scanning/detection of jdbi repositories.
* The scanned packages can be configured in the annotation. If no explicit configuration is done the package of the
* annotated element will be used as the sole base package.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(JdbiRepositoryRegistrar.class)
public @interface EnableJdbiRepositories {

/**
* The names of the base packages used for repository scanning in addition
* to the {@link #basePackages} and {@link #basePackageClasses} properties.
*/
String[] value() default {};

/**
* The names of the base packages used for repository scanning in addition
* to the {@link #value} and {@link #basePackageClasses} properties.
*/
String[] basePackages() default {};

/**
* The packages of these classes are used as base packages for repository
* scanning in addition to the {@link #value} and {@link #basePackages}
* properties.
*/
Class<?>[] basePackageClasses() default {};

/**
* Exact array of classes to consider as repositories. Overriding any of
* the values defined in {@link #value}, {@link #basePackages} or
* {@link #basePackageClasses}.
*/
Class<?>[] repositories() default {};

}
102 changes: 102 additions & 0 deletions spring5/src/main/java/org/jdbi/v3/spring5/JdbiJtaBinder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.spring5;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Stream;

import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.internal.UtilityClassException;
import org.jdbi.v3.core.internal.exceptions.Sneaky;
import org.jdbi.v3.sqlobject.SqlObject;

import static org.jdbi.v3.core.internal.JdbiClassUtils.EQUALS_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.HASHCODE_METHOD;
import static org.jdbi.v3.core.internal.JdbiClassUtils.TOSTRING_METHOD;

class JdbiJtaBinder {
private JdbiJtaBinder() {
throw new UtilityClassException();
}

/**
* Proxies the extension object to bind it to the jta framework. Creates and closes the handle if needed.
*/
static <E> E bind(Jdbi jdbi, Class<E> extensionType) {
InvocationHandler invocationHandler = createInvocationHandler(jdbi, extensionType);
return extensionType.cast(createProxy(invocationHandler, extensionType, SqlObject.class));
}

private static InvocationHandler createInvocationHandler(Jdbi jdbi, Class<?> extensionType) {
return (proxy, method, args) -> {
Handle handle = JdbiUtil.getHandle(jdbi);
try {
Object delegate = handle.attach(extensionType);
return invoke(delegate, method, args);
} finally {
JdbiUtil.closeIfNeeded(handle);
}
};
}

private static Object createProxy(InvocationHandler naiveHandler, Class<?> extensionType, Class<?>... extraTypes) {
InvocationHandler handler = (proxy, method, args) -> {
if (EQUALS_METHOD.equals(method)) {
return proxy == args[0];
}

if (HASHCODE_METHOD.equals(method)) {
return System.identityHashCode(proxy);
}

if (TOSTRING_METHOD.equals(method)) {
return "JdbiJta on demand proxy for " + extensionType.getName() + "@" + Integer.toHexString(System.identityHashCode(proxy));
}

return naiveHandler.invoke(proxy, method, args);

};

Class<?>[] types = Stream.of(
Stream.of(extensionType),
Arrays.stream(extensionType.getInterfaces()),
Arrays.stream(extraTypes))
.flatMap(Function.identity())
.distinct()
.toArray(Class[]::new);
return Proxy.newProxyInstance(extensionType.getClassLoader(), types, handler);
}

private static Object invoke(Object target, Method method, Object[] args) {
try {
if (Proxy.isProxyClass(target.getClass())) {
return Proxy.getInvocationHandler(target)
.invoke(target, method, args);
} else {
return MethodHandles.lookup().unreflect(method)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is expensive to lookup the method handle every time you invoke the method, but it's probably ok to fix that later when it shows up as an actual performance bottleneck.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concur, it is possible to have an initialized object to do this, but the code will end up to be more complex while this code is already complex.

.bindTo(target)
.invokeWithArguments(args);
}
} catch (Throwable t) {
throw Sneaky.throwAnyway(t);
}
}

}
38 changes: 38 additions & 0 deletions spring5/src/main/java/org/jdbi/v3/spring5/JdbiRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.spring5;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* When {@link EnableJdbiRepositories} is used,
* detected interfaces with this annotation will be regarded as a jdbi (sql-object) repository
* and are elligible for autowiring. The handle used for the execution is obtained and discarded using {@link JdbiUtil}
* and consequently will work in combination with spring managed transactions.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface JdbiRepository {
/**
* The qualifier (bean name) of the jdbi to use. Can be omitted if only one jdbi bean is available.
*/
String jdbiQualifier() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.spring5;

import org.jdbi.v3.core.Jdbi;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.Nullable;

public class JdbiRepositoryFactoryBean implements FactoryBean<Object>, ApplicationContextAware, BeanFactoryAware, InitializingBean {

private Class<?> objectType;
private String jdbiQualifier;
private BeanFactory beanFactory;

/**
* @return The jdbi instance to attach this repository to.
*/
protected Jdbi getJdbi() {
if (jdbiQualifier != null) {
return beanFactory.getBean(jdbiQualifier, Jdbi.class);
} else {
return beanFactory.getBean(Jdbi.class);
}
}

@Override
public Object getObject() {
return JdbiJtaBinder.bind(getJdbi(), objectType);
}

@Override
public Class<?> getObjectType() {
return objectType;
}

/**
* The object type of the repository.
*/
public void setObjectType(Class<?> objectType) {
this.objectType = objectType;
}

/**
* Set the jdbi qualifier.
* @param jdbiQualifier The name of the jdbi bean to bind the repository to.
* if <code>null</code> then no name will be specified during resolution.
*/
public void setJdbiQualifier(@Nullable String jdbiQualifier) {
this.jdbiQualifier = jdbiQualifier;
}

/**
* Verifies that the object type has been set
*/
@Override
public void afterPropertiesSet() {
if (objectType == null) {
throw new IllegalStateException("'type' property must be set");
}
}

@Override
public void setApplicationContext(ApplicationContext context) {
beanFactory = context;
}

@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}