From d2ed2c5c4da0a048d91dbb53c10bbc441c4007f3 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Wed, 13 May 2026 22:15:47 -0700 Subject: [PATCH] Use MethodHandle in storage layer --- .../handler/ComputedAttributesProvider.java | 22 +-- .../org/traccar/helper/ReflectionCache.java | 21 ++- .../org/traccar/storage/MemoryStorage.java | 17 +- .../org/traccar/storage/QueryBuilder.java | 77 ++++---- .../org/traccar/storage/QueryBuilderTest.java | 172 ++++++++++++++++++ 5 files changed, 254 insertions(+), 55 deletions(-) create mode 100644 src/test/java/org/traccar/storage/QueryBuilderTest.java diff --git a/src/main/java/org/traccar/handler/ComputedAttributesProvider.java b/src/main/java/org/traccar/handler/ComputedAttributesProvider.java index a98c85707..d9ce1917a 100644 --- a/src/main/java/org/traccar/handler/ComputedAttributesProvider.java +++ b/src/main/java/org/traccar/handler/ComputedAttributesProvider.java @@ -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); } }); diff --git a/src/main/java/org/traccar/helper/ReflectionCache.java b/src/main/java/org/traccar/helper/ReflectionCache.java index 6c67c7888..80323396e 100644 --- a/src/main/java/org/traccar/helper/ReflectionCache.java +++ b/src/main/java/org/traccar/helper/ReflectionCache.java @@ -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> 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; diff --git a/src/main/java/org/traccar/storage/MemoryStorage.java b/src/main/java/org/traccar/storage/MemoryStorage.java index 17dcdc6f0..dba9ee421 100644 --- a/src/main/java/org/traccar/storage/MemoryStorage.java +++ b/src/main/java/org/traccar/storage/MemoryStorage.java @@ -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); } } diff --git a/src/main/java/org/traccar/storage/QueryBuilder.java b/src/main/java/org/traccar/storage/QueryBuilder.java index 6a4f66fc8..e663692b5 100644 --- a/src/main/java/org/traccar/storage/QueryBuilder.java +++ b/src/main/java/org/traccar/storage/QueryBuilder.java @@ -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 { - void process(T object, ResultSet resultSet) throws ReflectiveOperationException, IOException, SQLException; + void process(T object, ResultSet resultSet) throws Throwable; } private void addProcessors( List> 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 processor : processors) { try { processor.process(object, retainedResultSet); - } catch (ReflectiveOperationException | IOException error) { + } catch (Throwable error) { LOGGER.warn("Set property error", error); } } diff --git a/src/test/java/org/traccar/storage/QueryBuilderTest.java b/src/test/java/org/traccar/storage/QueryBuilderTest.java new file mode 100644 index 000000000..e01935560 --- /dev/null +++ b/src/test/java/org/traccar/storage/QueryBuilderTest.java @@ -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 stream = query.executeQueryStreamed(TestEntity.class)) { + List 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 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 stream = query.executeQueryStreamed(TestEntity.class)) { + List 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 ids = insert.executeBatch(); + assertEquals(3, ids.size()); + } + + try (QueryBuilder query = QueryBuilder.create(config, dataSource, objectMapper, + "SELECT * FROM test_entity ORDER BY count"); + Stream stream = query.executeQueryStreamed(TestEntity.class)) { + List results = stream.toList(); + assertEquals(3, results.size()); + assertEquals("row0", results.get(0).getName()); + assertEquals("row2", results.get(2).getName()); + } + } + +}