本文是《轻量级 Java Web 框架架构设计》的系列博文。
为了开发一款轻量级的 Java Web 开发框架,我不惜放弃了我最深爱的 Hibernate。我非常感谢它这么多年来教会了我许多知识,让我不会再走弯路。既然没有弯路,那么什么才是捷径呢?那就是尽可能的简单,通过惯例(Convention)而不是配置(Configuration)。说通来讲就是完全不需要任何的配置,就连注解都不需要!
下面这个 Entity,我想足以让大家明白。
public class Product extends BaseEntity {
private long productTypeId;
private String productName;
private String productCode;
private int price;
private String description;
public long getProductTypeId() {
return productTypeId;
}
public void setProductTypeId(long productTypeId) {
this.productTypeId = productTypeId;
}
...
}
不需要 @Entity、@Table、@Column 这样的注解,同时也放弃了 @OneToMany 这类具有争议的功能。仅仅继承了 BaseEntity。对于 BaseEntity 以及有关 Entity 的架构思路,请参考我另外一篇博文《对 Entity 的初步构思》。
那么框架又是怎样加载 classpath 中所有的 Entity 呢?还是用代码来说明一切吧。
public class EntityHelper {
private static final Map<Class<?>, Map<String, String>> entityMap = new HashMap<Class<?>, Map<String, String>>();
static {
// 获取并遍历所有 Entity 类
List<Class<?>> entityClassList = ClassHelper.getClassList(BaseEntity.class);
for (Class<?> entityClass : entityClassList) {
// 获取并遍历该 Entity 类中的所有方法(不包括父类中的方法)
Field[] fields = entityClass.getDeclaredFields();
if (ArrayUtil.isNotEmpty(fields)) {
// 创建一个 Field Map(用于存放 Column 与 Field 的映射关系)
Map<String, String> fieldMap = new HashMap<String, String>();
for (Field field : fields) {
String fieldName = field.getName();
String columnName = StringUtil.toUnderline(fieldName); // 将驼峰风格替换为下划线风格
// 若 Field 与 Column 不同,则需要进行映射
if (!fieldName.equals(columnName)) {
fieldMap.put(columnName, fieldName);
}
}
// 将 Entity 类与 Field Map 放入 Entity Map 中
if (MapUtil.isNotEmpty(fieldMap)) {
entityMap.put(entityClass, fieldMap);
}
}
}
}
public static Map<Class<?>, Map<String, String>> getEntityMap() {
return entityMap;
}
}
这个 EntityHelper 类会自动初始化 Entity 类与 Column/Field 的映射关系,并放入 entityMap中。此后,就可以通过 getEntityMap() 这个静态方法随时获取这些映射关系了,这个也就实现了所谓的 ORM(当然是轻量级的了)。
以上这些都是框架为我们提供的基础支持,还有一个成员不得不说,那就是 DBHelper 类,有了它之后,程序员就可以无需面对 JDBC 返回的 ResultSet 了,而是面对 JavaBean(或者说是 Entity),也就是说,自动将 ResultSet 转换为 JavaBean。在这里我得感谢 Apache Commons DbUtils 类库,它真的是太好了,非常轻量级,我只用了一次就爱上它了!
public class DBHelper {
private static final BasicDataSource ds = new BasicDataSource();
private static final QueryRunner runner = new QueryRunner(ds);
static {
ds.setDriverClassName(ConfigHelper.getProperty("jdbc.driver"));
ds.setUrl(ConfigHelper.getProperty("jdbc.url"));
ds.setUsername(ConfigHelper.getProperty("jdbc.username"));
ds.setPassword(ConfigHelper.getProperty("jdbc.password"));
ds.setMaxActive(Integer.parseInt(ConfigHelper.getProperty("jdbc.max.active")));
}
// 获取数据源
public static DataSource getDataSource() {
return ds;
}
// 执行查询语句(返回一个对象)
public static <T> T queryBean(Class<T> cls, String sql, Object... params) {
Map<String, String> map = EntityHelper.getEntityMap().get(cls);
return DBUtil.queryBean(runner, cls, map, sql, params);
}
// 执行查询语句(返回多个对象)
public static <T> List<T> queryBeanList(Class<T> cls, String sql, Object... params) {
Map<String, String> map = EntityHelper.getEntityMap().get(cls);
return DBUtil.queryBeanList(runner, cls, map, sql, params);
}
// 执行更新语句(包括 UPDATE、INSERT、DELETE)
public static int update(String sql, Object... params) {
return DBUtil.update(runner, sql, params);
}
}
现在只是提供了最为典型的数据库操作,也就是 SELECT、UPDATE、INSERT、DELETE 了。将来或许还会有一些日常使用的方法,在这个 DBHelper 类里面进行扩展吧。
细心的读者可能已经看到了,在 DBHelper 中还有一个 DBUtil,这个类又是什么呢?难道就是对 Apache Commons DbUtils 的封装类?对头!
public class DBUtil {
// 查询(返回 Array)
public static Object[] queryArray(QueryRunner runner, String sql, Object... params) {
Object[] result = null;
try {
result = runner.query(sql, new ArrayHandler(), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询(返回 ArrayList)
public static List<Object[]> queryArrayList(QueryRunner runner, String sql, Object... params) {
List<Object[]> result = null;
try {
result = runner.query(sql, new ArrayListHandler(), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询(返回 Map)
public static Map<String, Object> queryMap(QueryRunner runner, String sql, Object... params) {
Map<String, Object> result = null;
try {
result = runner.query(sql, new MapHandler(), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询(返回 MapList)
public static List<Map<String, Object>> queryMapList(QueryRunner runner, String sql, Object... params) {
List<Map<String, Object>> result = null;
try {
result = runner.query(sql, new MapListHandler(), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询(返回 Bean)
public static <T> T queryBean(QueryRunner runner, Class<T> cls, Map<String, String> map, String sql, Object... params) {
T result = null;
try {
if (MapUtil.isNotEmpty(map)) {
result = runner.query(sql, new BeanHandler<T>(cls, new BasicRowProcessor(new BeanProcessor(map))), params);
} else {
result = runner.query(sql, new BeanHandler<T>(cls), params);
}
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询(返回 BeanList)
public static <T> List<T> queryBeanList(QueryRunner runner, Class<T> cls, Map<String, String> map, String sql, Object... params) {
List<T> result = null;
try {
if (MapUtil.isNotEmpty(map)) {
result = runner.query(sql, new BeanListHandler<T>(cls, new BasicRowProcessor(new BeanProcessor(map))), params);
} else {
result = runner.query(sql, new BeanListHandler<T>(cls), params);
}
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询指定列名的值(单条数据)
public static <T> T queryColumn(QueryRunner runner, String column, String sql, Object... params) {
T result = null;
try {
result = runner.query(sql, new ScalarHandler<T>(column), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询指定列名的值(多条数据)
public static <T> List<T> queryColumnList(QueryRunner runner, String column, String sql, Object... params) {
List<T> result = null;
try {
result = runner.query(sql, new ColumnListHandler<T>(column), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 查询指定列名对应的记录映射
public static <T> Map<T, Map<String, Object>> queryKeyMap(QueryRunner runner, String column, String sql, Object... params) {
Map<T, Map<String, Object>> result = null;
try {
result = runner.query(sql, new KeyedHandler<T>(column), params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
// 更新(包括 UPDATE、INSERT、DELETE,返回受影响的行数)
public static int update(QueryRunner runner, String sql, Object... params) {
int result = 0;
try {
result = runner.update(sql, params);
} catch (SQLException e) {
e.printStackTrace();
}
return result;
}
}
在 DBUtil 中,已经对 Apache Commons DbUtils 做了大量的封装,其似乎这个类对于程序员们来讲,不到万不得已,尽量不要用,最好还是用 DBHelper 吧。如果 DBHelper 中的方法不够用了,不妨给你的架构师提一个需求,让他去扩展 DBHelper。
那么 Service 层又是如何调用 DBHelper 的呢? 来看看这个 ProductService 吧。
public interface ProductService {
Product getProduct(long productId);
}
以上只是一个接口而已,实现类应该如何编写呢?别着急,往下看。
public class ProductServiceImpl extends BaseService implements ProductService {
@Override
public Product getProduct(long productId) {
String sql = SQLHelper.getSQL("select.product.id");
return DBHelper.queryBean(Product.class, sql, productId);
}
}
这里先从 SQLHelper 中拿到 SQL 语句(带有占位符“?”的),然后调用 DBHelper 的 queryBean() 方法,传入需要转换的 Java 类(Product.class)、SQL 语句(sql)、填充 SQL语句占位符的参数(productId)。返回值就是 Product 对象了。是不是很简单呢? 不对!这里为什么没有数据库连接呢? 这个问题留作读者自己思考吧。
有必要看看 SQLHelper 类是如何实现的,虽然它很简单,只是读取 properties 文件,并通过 key 获取 value(也即是 SQL)而已。
public class SQLHelper {
private static final Properties sqlProperties = FileUtil.loadPropertiesFile("sql.properties");
public static String getSQL(String key) {
String value = "";
if (sqlProperties.containsKey(key)) {
value = sqlProperties.getProperty(key);
} else {
System.err.println("Can not get property [" + key + "] in sql.properties file.");
}
return value;
}
}
以上代码真的很简单吧?那么,sql.properties 的内容还有悬念吗?
select.product.id = select * from product where id = ?
程序员们要做的就是写这个 sql.properties,还有 XxxService,以及 XxxServiceImpl 了。
万事俱备,只欠东风。最后来一个单元测试吧!
public class ProductServiceTest {
private ProductService productService = new ProductServiceImpl();
@Test
public void loginTest() {
long productId = 1;
Product product = productService.getProduct(productId);
Assert.assertNotNull(product);
}
}
这里没有对 productService 进行依赖注入,是否需要依赖注入呢?应该如何进行依赖注入呢?又是如何实现依赖注入的呢?这些问题留作下一篇博文与大家分享。
来一张高清无码类图吧。
灰色虚线圈出来得三个类是程序员要做的。灰色是 Base 类,黄色是 Helper 类,蓝色是 Util 类,还有一个 sql.properties 也是程序员要写的。
请大家不要吝啬,随便点评!
补充(2013-09-04)
非常感谢网友们的评价!对与 Entity 的结构有必要稍作修改,细节如下:
如果列名是 Java 关键字(例如:列名为 class),那么在 Entity 中一定不能定义一个名称为 class 的字段。所以,我还是采用了 @Column 注解,将列名写在注解中。
public class Product extends BaseEntity {
private long productTypeId;
private String productName;
private String productCode;
private int price;
private String description;
@Column("class")
private int clazz;
public long getProductTypeId() {
return productTypeId;
}
public void setProductTypeId(long productTypeId) {
this.productTypeId = productTypeId;
}
...
}
在 Product 实体的 clazz 字段上定义了一个 @Column("class") 注解,这就以为着将列名 class 映射为字段名 clazz。而且,如果使用了 @Column 注解,那么就不会采用默认的映射规则(用“下划线风格”的列名映射“驼峰风格”的字段名)。那么这又是如何实现的呢?对 EntityHelper 稍作修改即可实现。
...
for (Field field : fields) {
String fieldName = field.getName();
String columnName;
// 若该字段上存在 @Column 注解,则优先获取注解中的列名
if (field.isAnnotationPresent(Column.class)) {
columnName = field.getAnnotation(Column.class).value();
} else {
columnName = StringUtil.toUnderline(fieldName); // 将驼峰风格替换为下划线风格
}
// 若字段名与列名不同,则需要进行映射
if (!fieldName.equals(columnName)) {
fieldMap.put(columnName, fieldName);
}
}
...
通过一个 if...else... 就轻易地解决了此问题。
那么肯定会有人要问:如果数据库的表名是 Java 关键字,那么一定会与 Entity 的名称相冲突,那是否应该在 Entity 上标注一个 @Table("表名") 注解呢?
我个人认为是不需要这样做的,为什么呢?因为 SQL 语句都是程序员自己编写的,并统一写在 sql.properties 文件中,在 SQL 语句上用的就是表名,我们需要映射的是列名,仅此而已。
至于 Entity 中有些字段是否一定要与列名对应?这个问题,我还要细想一下,也请广大网友给出建议。