Building a Minimalistic MyBatis Implementation
Introduction
MyBatis serves as a persistence framework that simplifies database operations by eliminating repetitive tasks associated with traditional JDBC usage. It allows developers to focus more on business logic rather than boilerplate code, offering flexible SQL construction and result mapping capabilities.
This article walks through building a simplified version of MyBatis that demonstrates core features including:
- Streamlined database interactions
- Flexible SQL statement handling
- Parameter binding
- Result set processing
JDBC Overview
As MyBatis is built upon JDBC, we begin by examining standard JDBC practices before applying encapsulation techniques.
Entity Class
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
Standard JDBC Connection
public class DatabaseConnector {
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
public static Connection getConnection() throws SQLException {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new SQLException("Driver not found", e);
}
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
}
Manual JDBC Query Example
Connection conn = DatabaseConnector.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM user WHERE name = ?");
stmt.setString(1, "Zhang San");
ResultSet rs = stmt.executeQuery();
List<User> users = new ArrayList<>();
while(rs.next()) {
User user = new User();
user.setAge(rs.getInt("age"));
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
users.add(user);
}
System.out.println(users);
Core Implementation
We now proceed to implement a simplified MyBatis-like mechanism using proxy-based invocation.
Mapper Interface
public interface UserMapper {
@Select("SELECT * FROM user WHERE name = #{name} OR age = #{age}")
List<User> findAllUsers(@Param("name") String name, @Param("age") Integer age);
@Select("SELECT * FROM user WHERE name = #{name}")
User findUser(@Param("name") String name);
}
Annotation Definitions
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {
String value();
}
Proxy Factory
public class MapperProxyFactory {
private static final Map<Class<?>, TypeHandler<?>> TYPE_HANDLER_MAP = new HashMap<>();
static {
TYPE_HANDLER_MAP.put(Integer.class, new IntegerTypeHandler());
TYPE_HANDLER_MAP.put(String.class, new StringTypeHandler());
}
public static <T> T getMapper(Class<T> mapperInterface) {
return (T) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{mapperInterface},
(proxy, method, args) -> {
Select selectAnnotation = method.getAnnotation(Select.class);
String sql = selectAnnotation.value();
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser parser = new GenericTokenParser("#{", "}", tokenHandler);
String parsedSql = parser.parse(sql);
Connection conn = DatabaseConnector.getConnection();
PreparedStatement stmt = conn.prepareStatement(parsedSql);
Map<String, Object> paramMap = new HashMap<>();
Parameter[] params = method.getParameters();
for (int i = 0; i < params.length; i++) {
Param param = params[i].getAnnotation(Param.class);
String key = param.value();
paramMap.put(key, args[i]);
paramMap.put(params[i].getName(), args[i]);
}
List<ParameterMapping> mappings = tokenHandler.getParameterMappings();
for (int i = 0; i < mappings.size(); i++) {
String property = mappings.get(i).getProperty();
Object value = paramMap.get(property);
Class<?> valueType = value.getClass();
TYPE_HANDLER_MAP.get(valueType).setParameter(stmt, i + 1, value);
}
stmt.execute();
ResultSet rs = stmt.getResultSet();
List<Object> results = new ArrayList<>();
Type returnType = method.getGenericReturnType();
Class<?> resultType;
boolean isCollection = false;
if (returnType instanceof Class<?>) {
resultType = (Class<?>) returnType;
} else if (returnType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) returnType;
resultType = (Class<?>) pType.getActualTypeArguments()[0];
isCollection = true;
} else {
throw new UnsupportedOperationException("Unsupported return type");
}
ResultSetMetaData metaData = rs.getMetaData();
List<String> columns = new ArrayList<>();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
columns.add(metaData.getColumnName(i));
}
Method[] setters = resultType.getMethods();
Map<String, Method> setterMap = new HashMap<>();
for (Method m : setters) {
if (m.getName().startsWith("set")) {
String prop = m.getName().substring(3);
String lowerProp = Character.toLowerCase(prop.charAt(0)) + prop.substring(1);
setterMap.put(lowerProp, m);
}
}
while (rs.next()) {
Object instance = resultType.newInstance();
for (String col : columns) {
Method setter = setterMap.get(col);
if (setter != null) {
Class<?> argType = setter.getParameterTypes()[0];
TypeHandler<?> handler = TYPE_HANDLER_MAP.get(argType);
Object val = handler.getResult(rs, col);
setter.invoke(instance, val);
}
}
results.add(instance);
}
return isCollection ? results : results.isEmpty() ? null : results.get(0);
}
);
}
}
SQL Parsing Component
public class GenericTokenParser {
private final String openToken;
private final String closeToken;
private final TokenHandler handler;
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
public String parse(String text) {
if (text == null || text.isEmpty()) return "";
int start = text.indexOf(openToken);
if (start == -1) return text;
char[] src = text.toCharArray();
int offset = 0;
StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
if (expression == null) expression = new StringBuilder();
else expression.setLength(0);
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1 && src[end - 1] == '\\') {
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
}
if (end == -1) {
builder.append(src, start, src.length - start);
offset = src.length;
} else {
expression.append(src, offset, end - offset);
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
Parameter Mapping
public class ParameterMapping {
private final String property;
public ParameterMapping(String property) {
this.property = property;
}
public String getProperty() {
return property;
}
}
Token Handler for Parameters
public class ParameterMappingTokenHandler implements TokenHandler {
private final List<ParameterMapping> parameterMappings = new ArrayList<>();
@Override
public String handleToken(String content) {
parameterMappings.add(new ParameterMapping(content));
return "?";
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
}
public interface TokenHandler {
String handleToken(String content);
}
Type Handlers
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, Integer index, T value) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
}
public class StringTypeHandler implements TypeHandler<String> {
@Override
public void setParameter(PreparedStatement ps, Integer index, String value) throws SQLException {
ps.setString(index, value);
}
@Override
public String getResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}
}
public class IntegerTypeHandler implements TypeHandler<Integer> {
@Override
public void setParameter(PreparedStatement ps, Integer index, Integer value) throws SQLException {
ps.setInt(index, value);
}
@Override
public Integer getResult(ResultSet rs, String columnName) throws SQLException {
return rs.getInt(columnName);
}
}