mirror of
https://github.com/traccar/traccar.git
synced 2026-05-19 14:18:23 -04:00
Use MethodHandle in storage layer
This commit is contained in:
@@ -33,8 +33,7 @@ import org.traccar.model.Device;
|
||||
import org.traccar.model.Position;
|
||||
import org.traccar.session.cache.CacheManager;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -96,26 +95,27 @@ public class ComputedAttributesProvider {
|
||||
}
|
||||
}
|
||||
Position last = includeLastAttributes ? cacheManager.getPosition(position.getDeviceId()) : null;
|
||||
ReflectionCache.getProperties(Position.class, "get").forEach((key, value) -> {
|
||||
Method method = value.method();
|
||||
String name = Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
|
||||
ReflectionCache.getProperties(Position.class, "get").forEach((name, property) -> {
|
||||
MethodHandle handle = property.handle();
|
||||
try {
|
||||
if (!method.getReturnType().equals(Map.class)) {
|
||||
result.set(name, method.invoke(position));
|
||||
Object positionValue = handle.invokeExact((Object) position);
|
||||
Object lastValue = last != null ? handle.invokeExact((Object) last) : null;
|
||||
if (!property.type().equals(Map.class)) {
|
||||
result.set(name, positionValue);
|
||||
if (last != null) {
|
||||
result.set(prefixAttribute("last", name), method.invoke(last));
|
||||
result.set(prefixAttribute("last", name), lastValue);
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) method.invoke(position)).entrySet()) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) positionValue).entrySet()) {
|
||||
result.set((String) entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (last != null) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) method.invoke(last)).entrySet()) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) lastValue).entrySet()) {
|
||||
result.set(prefixAttribute("last", (String) entry.getKey()), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException error) {
|
||||
} catch (Throwable error) {
|
||||
LOGGER.warn("Attribute reflection error", error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,6 +18,9 @@ package org.traccar.helper;
|
||||
import org.traccar.storage.QueryIgnore;
|
||||
|
||||
import java.beans.Introspector;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
@@ -30,10 +33,14 @@ public final class ReflectionCache {
|
||||
private ReflectionCache() {
|
||||
}
|
||||
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup();
|
||||
private static final MethodType SETTER_TYPE = MethodType.methodType(void.class, Object.class, Object.class);
|
||||
private static final MethodType GETTER_TYPE = MethodType.methodType(Object.class, Object.class);
|
||||
|
||||
private record Key(Class<?> clazz, String type) {
|
||||
}
|
||||
|
||||
public record PropertyMethod(Method method, boolean queryIgnore, String lowerCaseName) {
|
||||
public record PropertyMethod(Class<?> type, boolean queryIgnore, String lowerCaseName, MethodHandle handle) {
|
||||
}
|
||||
|
||||
private static final Map<Key, Map<String, PropertyMethod>> CACHE = new ConcurrentHashMap<>();
|
||||
@@ -48,9 +55,17 @@ public final class ReflectionCache {
|
||||
if (method.getName().startsWith(key.type()) && parameters.length == parameterCount
|
||||
&& !method.getName().equals("getClass")) {
|
||||
String name = Introspector.decapitalize(method.getName().substring(3));
|
||||
MethodHandle handle;
|
||||
try {
|
||||
handle = LOOKUP.unreflect(method)
|
||||
.asType(parameterCount == 1 ? SETTER_TYPE : GETTER_TYPE);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
Class<?> propertyType = parameterCount == 1 ? parameters[0] : method.getReturnType();
|
||||
properties.put(name, new PropertyMethod(
|
||||
method, method.isAnnotationPresent(QueryIgnore.class),
|
||||
name.toLowerCase(Locale.ROOT)));
|
||||
propertyType, method.isAnnotationPresent(QueryIgnore.class),
|
||||
name.toLowerCase(Locale.ROOT), handle));
|
||||
}
|
||||
}
|
||||
return properties;
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.traccar.model.Server;
|
||||
import org.traccar.storage.query.Condition;
|
||||
import org.traccar.storage.query.Request;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -117,10 +117,10 @@ public class MemoryStorage extends Storage {
|
||||
}
|
||||
|
||||
private Object retrieveValue(Object object, String key) {
|
||||
MethodHandle handle = ReflectionCache.getProperties(object.getClass(), "get").get(key).handle();
|
||||
try {
|
||||
Method method = ReflectionCache.getProperties(object.getClass(), "get").get(key).method();
|
||||
return method.invoke(object);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return handle.invokeExact(object);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@@ -144,13 +144,14 @@ public class MemoryStorage extends Storage {
|
||||
var getters = ReflectionCache.getProperties(entity.getClass(), "get");
|
||||
var setters = ReflectionCache.getProperties(entity.getClass(), "set");
|
||||
for (String column : request.getColumns().getColumns(entity.getClass(), "get")) {
|
||||
MethodHandle setter = setters.get(column).handle();
|
||||
MethodHandle getter = getters.get(column).handle();
|
||||
try {
|
||||
Method setter = setters.get(column).method();
|
||||
Object value = getters.get(column).method().invoke(entity);
|
||||
Object value = getter.invokeExact(entity);
|
||||
for (Object object : items) {
|
||||
setter.invoke(object, value);
|
||||
setter.invokeExact(object, value);
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.traccar.storage;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -25,9 +24,8 @@ import org.traccar.helper.ReflectionCache;
|
||||
import org.traccar.model.Permission;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
@@ -160,61 +158,75 @@ public final class QueryBuilder implements AutoCloseable {
|
||||
try {
|
||||
for (int index = 0; index < columns.size(); index++) {
|
||||
String column = columns.get(index);
|
||||
Method method = ReflectionCache.getProperties(object.getClass(), "get").get(column).method();
|
||||
if (method.getReturnType().equals(boolean.class)) {
|
||||
setBoolean(index, (Boolean) method.invoke(object));
|
||||
} else if (method.getReturnType().equals(int.class)) {
|
||||
setInteger(index, (Integer) method.invoke(object));
|
||||
} else if (method.getReturnType().equals(long.class)) {
|
||||
setLong(index, (Long) method.invoke(object), column.endsWith("Id"));
|
||||
} else if (method.getReturnType().equals(double.class)) {
|
||||
setDouble(index, (Double) method.invoke(object));
|
||||
} else if (method.getReturnType().equals(String.class)) {
|
||||
setString(index, (String) method.invoke(object));
|
||||
} else if (method.getReturnType().equals(Date.class)) {
|
||||
setDate(index, (Date) method.invoke(object));
|
||||
} else if (method.getReturnType().equals(byte[].class)) {
|
||||
setBlob(index, (byte[]) method.invoke(object));
|
||||
var property = ReflectionCache.getProperties(object.getClass(), "get").get(column);
|
||||
Class<?> returnType = property.type();
|
||||
Object value = property.handle().invokeExact(object);
|
||||
if (returnType.equals(boolean.class)) {
|
||||
setBoolean(index, (Boolean) value);
|
||||
} else if (returnType.equals(int.class)) {
|
||||
setInteger(index, (Integer) value);
|
||||
} else if (returnType.equals(long.class)) {
|
||||
setLong(index, (Long) value, column.endsWith("Id"));
|
||||
} else if (returnType.equals(double.class)) {
|
||||
setDouble(index, (Double) value);
|
||||
} else if (returnType.equals(String.class)) {
|
||||
setString(index, (String) value);
|
||||
} else if (returnType.equals(Date.class)) {
|
||||
setDate(index, (Date) value);
|
||||
} else if (returnType.equals(byte[].class)) {
|
||||
setBlob(index, (byte[]) value);
|
||||
} else {
|
||||
setString(index, objectMapper.writeValueAsString(method.invoke(object)));
|
||||
setString(index, objectMapper.writeValueAsString(value));
|
||||
}
|
||||
}
|
||||
} catch (ReflectiveOperationException | JsonProcessingException e) {
|
||||
} catch (Throwable e) {
|
||||
LOGGER.warn("Set object error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private interface ResultSetProcessor<T> {
|
||||
void process(T object, ResultSet resultSet) throws ReflectiveOperationException, IOException, SQLException;
|
||||
void process(T object, ResultSet resultSet) throws Throwable;
|
||||
}
|
||||
|
||||
private <T> void addProcessors(
|
||||
List<ResultSetProcessor<T>> processors,
|
||||
final Class<?> parameterType, final Method method, final int columnIndex) {
|
||||
final Class<?> parameterType, final MethodHandle handle, final int columnIndex) {
|
||||
if (parameterType.equals(boolean.class)) {
|
||||
processors.add((object, resultSet) -> method.invoke(object, resultSet.getBoolean(columnIndex)));
|
||||
processors.add((object, resultSet) -> {
|
||||
handle.invokeExact(object, (Object) resultSet.getBoolean(columnIndex));
|
||||
});
|
||||
} else if (parameterType.equals(int.class)) {
|
||||
processors.add((object, resultSet) -> method.invoke(object, resultSet.getInt(columnIndex)));
|
||||
processors.add((object, resultSet) -> {
|
||||
handle.invokeExact(object, (Object) resultSet.getInt(columnIndex));
|
||||
});
|
||||
} else if (parameterType.equals(long.class)) {
|
||||
processors.add((object, resultSet) -> method.invoke(object, resultSet.getLong(columnIndex)));
|
||||
processors.add((object, resultSet) -> {
|
||||
handle.invokeExact(object, (Object) resultSet.getLong(columnIndex));
|
||||
});
|
||||
} else if (parameterType.equals(double.class)) {
|
||||
processors.add((object, resultSet) -> method.invoke(object, resultSet.getDouble(columnIndex)));
|
||||
processors.add((object, resultSet) -> {
|
||||
handle.invokeExact(object, (Object) resultSet.getDouble(columnIndex));
|
||||
});
|
||||
} else if (parameterType.equals(String.class)) {
|
||||
processors.add((object, resultSet) -> method.invoke(object, resultSet.getString(columnIndex)));
|
||||
processors.add((object, resultSet) -> {
|
||||
handle.invokeExact(object, (Object) resultSet.getString(columnIndex));
|
||||
});
|
||||
} else if (parameterType.equals(Date.class)) {
|
||||
processors.add((object, resultSet) -> {
|
||||
Timestamp timestamp = resultSet.getTimestamp(columnIndex);
|
||||
if (timestamp != null) {
|
||||
method.invoke(object, new Date(timestamp.getTime()));
|
||||
handle.invokeExact(object, (Object) new Date(timestamp.getTime()));
|
||||
}
|
||||
});
|
||||
} else if (parameterType.equals(byte[].class)) {
|
||||
processors.add((object, resultSet) -> method.invoke(object, (Object) resultSet.getBytes(columnIndex)));
|
||||
processors.add((object, resultSet) -> {
|
||||
handle.invokeExact(object, (Object) resultSet.getBytes(columnIndex));
|
||||
});
|
||||
} else {
|
||||
processors.add((object, resultSet) -> {
|
||||
String value = resultSet.getString(columnIndex);
|
||||
if (value != null && !value.isEmpty()) {
|
||||
method.invoke(object, objectMapper.readValue(value, parameterType));
|
||||
handle.invokeExact(object, (Object) objectMapper.readValue(value, parameterType));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -243,8 +255,7 @@ public final class QueryBuilder implements AutoCloseable {
|
||||
for (var property : ReflectionCache.getProperties(clazz, "set").values()) {
|
||||
Integer columnIndex = columnIndexes.get(property.lowerCaseName());
|
||||
if (columnIndex != null) {
|
||||
Method method = property.method();
|
||||
addProcessors(processors, method.getParameterTypes()[0], method, columnIndex);
|
||||
addProcessors(processors, property.type(), property.handle(), columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +272,7 @@ public final class QueryBuilder implements AutoCloseable {
|
||||
for (ResultSetProcessor<T> processor : processors) {
|
||||
try {
|
||||
processor.process(object, retainedResultSet);
|
||||
} catch (ReflectiveOperationException | IOException error) {
|
||||
} catch (Throwable error) {
|
||||
LOGGER.warn("Set property error", error);
|
||||
}
|
||||
}
|
||||
|
||||
172
src/test/java/org/traccar/storage/QueryBuilderTest.java
Normal file
172
src/test/java/org/traccar/storage/QueryBuilderTest.java
Normal file
@@ -0,0 +1,172 @@
|
||||
package org.traccar.storage;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.traccar.config.Config;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class QueryBuilderTest {
|
||||
|
||||
private DataSource dataSource;
|
||||
private Config config;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public static class TestEntity {
|
||||
private long id;
|
||||
private boolean active;
|
||||
private int count;
|
||||
private long deviceId;
|
||||
private double speed;
|
||||
private String name;
|
||||
private Date fixTime;
|
||||
private byte[] data;
|
||||
|
||||
public long getId() { return id; }
|
||||
public void setId(long id) { this.id = id; }
|
||||
public boolean getActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
public int getCount() { return count; }
|
||||
public void setCount(int count) { this.count = count; }
|
||||
public long getDeviceId() { return deviceId; }
|
||||
public void setDeviceId(long deviceId) { this.deviceId = deviceId; }
|
||||
public double getSpeed() { return speed; }
|
||||
public void setSpeed(double speed) { this.speed = speed; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public Date getFixTime() { return fixTime; }
|
||||
public void setFixTime(Date fixTime) { this.fixTime = fixTime; }
|
||||
public byte[] getData() { return data; }
|
||||
public void setData(byte[] data) { this.data = data; }
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws Exception {
|
||||
JdbcDataSource h2 = new JdbcDataSource();
|
||||
h2.setURL("jdbc:h2:mem:querybuildertest;DB_CLOSE_DELAY=-1");
|
||||
dataSource = h2;
|
||||
config = new Config();
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
statement.execute("DROP TABLE IF EXISTS test_entity");
|
||||
statement.execute(
|
||||
"CREATE TABLE test_entity ("
|
||||
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY,"
|
||||
+ "active BOOLEAN,"
|
||||
+ "count INTEGER,"
|
||||
+ "deviceId BIGINT,"
|
||||
+ "speed DOUBLE,"
|
||||
+ "name VARCHAR(255),"
|
||||
+ "fixTime TIMESTAMP,"
|
||||
+ "data VARBINARY(255))");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripsAllPrimitiveAndReferenceTypes() throws Exception {
|
||||
Date now = new Date(1700000000000L);
|
||||
byte[] payload = {1, 2, 3, 4, 5};
|
||||
|
||||
try (QueryBuilder insert = QueryBuilder.create(config, dataSource, objectMapper,
|
||||
"INSERT INTO test_entity(active, count, deviceId, speed, name, fixTime, data) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?)", true)) {
|
||||
insert.setBoolean(0, true);
|
||||
insert.setInteger(1, 42);
|
||||
insert.setLong(2, 7L);
|
||||
insert.setDouble(3, 99.5);
|
||||
insert.setString(4, "hello");
|
||||
insert.setDate(5, now);
|
||||
insert.setBlob(6, payload);
|
||||
long id = insert.executeUpdate();
|
||||
assertTrue(id > 0);
|
||||
}
|
||||
|
||||
try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper,
|
||||
"SELECT * FROM test_entity");
|
||||
Stream<TestEntity> stream = query.executeQueryStreamed(TestEntity.class)) {
|
||||
List<TestEntity> results = stream.toList();
|
||||
assertEquals(1, results.size());
|
||||
TestEntity entity = results.get(0);
|
||||
assertEquals(true, entity.getActive());
|
||||
assertEquals(42, entity.getCount());
|
||||
assertEquals(7L, entity.getDeviceId());
|
||||
assertEquals(99.5, entity.getSpeed(), 0.0);
|
||||
assertEquals("hello", entity.getName());
|
||||
assertEquals(now, entity.getFixTime());
|
||||
assertArrayEquals(payload, entity.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setObjectInsertAndReadBack() throws Exception {
|
||||
Date now = new Date(1700000000000L);
|
||||
TestEntity entity = new TestEntity();
|
||||
entity.setActive(false);
|
||||
entity.setCount(7);
|
||||
entity.setDeviceId(123L);
|
||||
entity.setSpeed(12.5);
|
||||
entity.setName("world");
|
||||
entity.setFixTime(now);
|
||||
entity.setData(new byte[] {9, 8, 7});
|
||||
|
||||
List<String> columns = List.of("active", "count", "deviceId", "speed", "name", "fixTime", "data");
|
||||
try (QueryBuilder insert = QueryBuilder.create(config, dataSource, objectMapper,
|
||||
"INSERT INTO test_entity(active, count, deviceId, speed, name, fixTime, data) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?)", true)) {
|
||||
insert.setObject(entity, columns);
|
||||
insert.executeUpdate();
|
||||
}
|
||||
|
||||
try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper,
|
||||
"SELECT * FROM test_entity");
|
||||
Stream<TestEntity> stream = query.executeQueryStreamed(TestEntity.class)) {
|
||||
List<TestEntity> results = stream.toList();
|
||||
assertEquals(1, results.size());
|
||||
TestEntity loaded = results.get(0);
|
||||
assertEquals(false, loaded.getActive());
|
||||
assertEquals(7, loaded.getCount());
|
||||
assertEquals(123L, loaded.getDeviceId());
|
||||
assertEquals(12.5, loaded.getSpeed(), 0.0);
|
||||
assertEquals("world", loaded.getName());
|
||||
assertEquals(now, loaded.getFixTime());
|
||||
assertArrayEquals(new byte[] {9, 8, 7}, loaded.getData());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executeBatchInsertsMultipleRows() throws Exception {
|
||||
try (QueryBuilder insert = QueryBuilder.create(config, dataSource, objectMapper,
|
||||
"INSERT INTO test_entity(name, count) VALUES (?, ?)", true)) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
insert.setString(0, "row" + i);
|
||||
insert.setInteger(1, i);
|
||||
insert.addBatch();
|
||||
}
|
||||
List<Long> ids = insert.executeBatch();
|
||||
assertEquals(3, ids.size());
|
||||
}
|
||||
|
||||
try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper,
|
||||
"SELECT * FROM test_entity ORDER BY count");
|
||||
Stream<TestEntity> stream = query.executeQueryStreamed(TestEntity.class)) {
|
||||
List<TestEntity> results = stream.toList();
|
||||
assertEquals(3, results.size());
|
||||
assertEquals("row0", results.get(0).getName());
|
||||
assertEquals("row2", results.get(2).getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user