0%

现在成熟的ORM比比皆是,这里只介绍Dapper的使用(最起码我在使用它,已经运用到项目中,小伙伴们反馈还可以)。

优点:

1、开源、轻量、小巧、上手容易。

2、支持的数据库还蛮多的, Mysql,SqlLite,Sqlserver,Oracle等一系列的数据库。

3、Dapper原理通过Emit反射IDataReader的序列队列来快速的得到和产生对象。性能貌似很牛逼的样子

缺点:

作为一款ORM太过于轻量级了,根据对象自动生成sql的功能还是空白,需要自己来扩展,

当然这也是优点,  好声音的导师们经常说某人就是张白纸……  

因此针对Dapper已经有很多成熟的扩展项目了,Dapper.Rainbow、Dapper.Contrib,DapperExtensions。

我们这里介绍的是DapperExtensions

dapper-dot-net源码:https://github.com/StackExchange/dapper-dot-net   (更新频率快,项目包含了各种除了Dapper-Extensions的 扩展项目)

Dapper-Extensions 源码:https://github.com/tmsmith/Dapper-Extensions

Dapper-Extensions的优点

1、开源

2、针对Dapper封装了常用的CRUD方法,有独立的查询语法。

3、需要映射的实体类本身0配置,无需加特性什么的。是通过独立的映射类来处理,可以设置类映射到DB的别名,字段的别名等等。

Dapper-Extensions的缺点:

1、好几年没更新了

2、不支持oracle(木有oracle的方言,已经搞定)  

3、不能同时支持多种数据库(已经搞定)

4、部分代码有些bug(发现的都搞定了)

下面先简单介绍一下Dapper的基本语法。

Dapper就一个.cs文件,可以放到项目代码中直接编译,也可以直接引用DLL文件。

Dapper对DB的操作依赖于Connection,为了支持多库,咱们用 IDbConnection conn

using (IDbConnection conn = GetConnection())
{ const string query = “select * from XO order by id desc”; return conn.Query(query,null);
}

下面是带参数的语法

复制代码

int xoID=666; //变量主键
using (IDbConnection conn = GetConnection())
{ const string query = “select * from XO where Id=@MyID”; return conn.Query(query, new { MyID = xoID});

}

复制代码

各种方法都重载了事务的操作,一般的数据库操作都支持。但是每次执行都需要传递sql,而且每次都要使用Using,看着不爽啊, 这…… 

好吧下面简单介绍下使用Dapper-Extensions的基本语法(在Dapper-Extensions  的基础上用了Repository模式,代码效果如下)。

复制代码

        //实体类
        DemoEntity entity = new DemoEntity(); //根据实体主键删除
        this.Delete<DemoEntity>(entity); //根据主键ID删除
        this.Delete<DemoEntity>(1); //增加
        this.Insert<DemoEntity>(entity); //更新
        bool result = this.Update<DemoEntity>(entity); //根据主键返回实体
        entity = this.GetById<DemoEntity>(1); //返回 行数
        this.Count<DemoEntity>(new { ID = 1 }); //查询所有
        IEnumerable<DemoEntity> list = this.GetAll<DemoEntity>();

        IList<ISort> sort = new List<ISort>();
        sort.Add(new Sort { PropertyName = "ID", Ascending = false }); //条件查询
        list = this.GetList<DemoEntity>(new { ID = 1, Name = "123" }, sort); //orm 拼接条件 查询
        IList<IPredicate> predList = new List<IPredicate>();
        predList.Add(Predicates.Field<DemoEntity>(p => p.Name, Operator.Like, "不知道%"));
        predList.Add(Predicates.Field<DemoEntity>(p => p.ID, Operator.Eq, 1));
        IPredicateGroup predGroup \= Predicates.Group(GroupOperator.And, predList.ToArray());

        

        list \= this.GetList<DemoEntity>(predGroup); //分页查询
        long allRowsCount = 0; this.GetPageList<DemoEntity>(1, 10, out allRowsCount, new { ID = 1 }, sort);

复制代码

在说ORM之前,还是要说一下HY.DataAccess这个模块

 

这个模块是对数据访问提供的一个Helper的功能,里面包含了 各种DB的SqlHelper,分页。

DBHelper 都继承自IDBHelper.cs

using System.Data.Common; using System.Data; namespace HY.DataAccess
{ ///


/// 提供对数据库的基本操作,连接字符串需要在数据库配置。 ///

public interface IDBHelper
{ ///
/// 生成分页SQL语句 ///

///
///
///
///
///
///
string GetPagingSql(int pageIndex, int pageSize, string selectSql, string sqlCount, string orderBy); ///
/// 开始一个事务 ///

///
DbTransaction BeginTractionand(); ///
/// 开始一个事务 ///

/// 数据库连接字符key
DbTransaction BeginTractionand(string connKey); ///
/// 回滚事务 ///

/// 要回滚的事务
void RollbackTractionand(DbTransaction dbTransaction); ///
/// 结束并确认事务 ///

/// 要结束的事务
void CommitTractionand(DbTransaction dbTransaction); #region DataSet

    /// <summary>
    /// 执行sql语句,ExecuteDataSet 返回DataSet /// </summary>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    DataSet ExecuteDataSet(string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteDataSet 返回DataSet /// </summary>
    /// <param name="connKey">数据库连接字符key</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    DataSet ExecuteDataSet(string connKey, string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteDataSet 返回DataSet /// </summary>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    DataSet ExecuteDataSet(string commandText, CommandType commandType, params DbParameter\[\] parameterValues); /// <summary>
    /// 执行sql语句,ExecuteDataSet 返回DataSet /// </summary>
    /// <param name="connKey">数据库连接字符key</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    DataSet ExecuteDataSet(string connKey, string commandText, CommandType commandType, params DbParameter\[\] parameterValues); #endregion

    #region ExecuteNonQuery

    /// <summary>
    /// 执行sql语句,返回影响的行数 /// </summary>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    int ExecuteNonQuery(string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,返回影响的行数 /// </summary>
    /// <param name="connKey">数据库连接字符key</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    int ExecuteNonQuery(string connKey, string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,返回影响的行数 /// </summary>
    /// <param name="trans">事务对象</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    int ExecuteNonQuery(DbTransaction trans, string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,返回影响的行数 /// </summary>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    int ExecuteNonQuery(string commandText, CommandType commandType, params DbParameter\[\] parameterValues); /// <summary>
    /// 执行sql语句,返回影响的行数 /// </summary>
    /// <param name="connKey">数据库连接字符key</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    int ExecuteNonQuery(string connKey, string commandText, CommandType commandType, params DbParameter\[\] parameterValues); /// <summary>
    /// 执行sql语句,返回影响的行数 /// </summary>
    /// <param name="trans">事务对象</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    int ExecuteNonQuery(DbTransaction trans, string commandText, CommandType commandType, params DbParameter\[\] parameterValues); #endregion

    #region IDataReader

    /// <summary>
    /// 执行sql语句,ExecuteReader 返回IDataReader /// </summary>   
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    IDataReader ExecuteReader(string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteReader 返回IDataReader /// </summary> 
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    IDataReader ExecuteReader(string commandText, CommandType commandType, params DbParameter\[\] parameterValues); /// <summary>
    /// 执行sql语句,ExecuteReader 返回IDataReader /// </summary>
    /// <param name="connKey">数据库连接字符key</param>        
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    IDataReader ExecuteReader(string connKey, string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteReader 返回IDataReader /// </summary>
    /// <param name="connKey">数据库连接字符key</param>        
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    IDataReader ExecuteReader(string connKey, string commandText, CommandType commandType, params DbParameter\[\] parameterValues); #endregion

    #region ExecuteScalar

    /// <summary>
    /// 执行sql语句,ExecuteScalar 返回第一行第一列的值 /// </summary>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    object ExecuteScalar(string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteScalar 返回第一行第一列的值 /// </summary>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    object ExecuteScalar(string commandText, CommandType commandType, params DbParameter\[\] parameterValues); /// <summary>
    /// 执行sql语句,ExecuteScalar 返回第一行第一列的值 /// </summary>
    /// <param name="trans">事务</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    object ExecuteScalar(DbTransaction trans, string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteScalar 返回第一行第一列的值 /// </summary>
    /// <param name="connKey">数据库连接字符key</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    object ExecuteScalar(string connKey, string commandText, CommandType commandType); /// <summary>
    /// 执行sql语句,ExecuteScalar 返回第一行第一列的值 /// </summary>
    /// <param name="connKey">数据库连接字符key</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    object ExecuteScalar(string connKey, string commandText, CommandType commandType, params DbParameter\[\] parameterValues); /// <summary>
    /// 执行sql语句,ExecuteScalar 返回第一行第一列的值 /// </summary>
    /// <param name="trans">事务</param>
    /// <param name="commandText">sql语句</param>
    /// <param name="commandType"></param>
    /// <param name="parameterValues">参数</param>
    /// <returns></returns>
    object ExecuteScalar(DbTransaction trans, string commandText, CommandType commandType, params DbParameter\[\] parameterValues); #endregion }

}

View Code

IDBSession.cs 对数据访问对象的定义

using System; using System.Data; namespace HY.DataAccess
{ ///


/// 数据库接口 ///

public interface IDatabase
{
IDbConnection Connection { get; }

    DatabaseType DatabaseType { get; } string ConnKey { get; }
} /// <summary>
/// 数据库类对象 /// </summary>
public class Database : IDatabase
{ public IDbConnection Connection { get; private set; } public DatabaseType DatabaseType { get; private set; } public string ConnKey { get; set; } public Database(IDbConnection connection)
    {
        Connection \= connection;
    } public Database(DatabaseType dbType, string connKey)
    {
        DatabaseType \= dbType;
        ConnKey \= connKey;
        Connection \= SqlConnectionFactory.CreateSqlConnection(dbType, connKey);
    }

} /// <summary>
/// 数据连接事务的Session接口 /// </summary>
public interface IDBSession : IDisposable
{ string ConnKey { get; }
    DatabaseType DatabaseType { get; }
    IDbConnection Connection { get; }
    IDbTransaction Transaction { get; }
    IDbTransaction Begin(IsolationLevel isolation \= IsolationLevel.ReadCommitted); void Commit(); void Rollback();
}

}

View Code

SqlConnectionFactory.cs 这个类是采用工厂模式创建DB连接的封装,代码如下:

using System; using System.Collections.Generic; using System.Configuration; using System.Data; namespace HY.DataAccess
{ public enum DatabaseType
{
SqlServer,
MySql,
Oracle,
DB2
} public class SqlConnectionFactory
{ public static IDbConnection CreateSqlConnection(DatabaseType dbType, string strKey)
{
IDbConnection connection = null; string strConn = ConfigurationManager.ConnectionStrings[strKey].ConnectionString; switch (dbType)
{ case DatabaseType.SqlServer:
connection = new System.Data.SqlClient.SqlConnection(strConn); break; case DatabaseType.MySql: //connection = new MySql.Data.MySqlClient.MySqlConnection(strConn); //break;
case DatabaseType.Oracle: //connection = new Oracle.DataAccess.Client.OracleConnection(strConn);
connection = new System.Data.OracleClient.OracleConnection(strConn); break; case DatabaseType.DB2:
connection = new System.Data.OleDb.OleDbConnection(strConn); break;
} return connection;
}
}
}

View Code

 ORM也不是万能的,比如做大数据的批量插入什么的,还是需要SqlHelper,加上有的人就喜欢DataTable或者DataSet。

所以SqlHelper作为根基,ORM作为辅助,万无一失啊。

下面说说ORM这块的实现方式。见下截图

IDataServiceRepository.cs(提供业务层使用,里面的方法不支持传递sql,包含sql的语句最好还是放在数据层操作的好)

using System.Collections.Generic; using System.Data; using DapperExtensions; using HY.DataAccess; namespace HY.ORM
{ public interface IDataServiceRepository
{
IDBSession DBSession { get; }

    T GetById<T>(dynamic primaryId) where T : class;
    IEnumerable<T> GetByIds<T>(IList<dynamic> ids) where T : class;
    IEnumerable<T> GetAll<T>() where T : class; int Count<T>(object predicate, bool buffered = false) where T : class; //lsit
    IEnumerable<T> GetList<T>(object predicate = null, IList<ISort> sort = null, bool buffered = false) where T : class;

    IEnumerable<T> GetPageList<T>(int pageIndex, int pageSize, out long allRowsCount, object predicate = null, IList<ISort> sort = null, bool buffered = true) where T : class;


    dynamic Insert<T>(T entity, IDbTransaction transaction = null) where T : class; bool InsertBatch<T>(IEnumerable<T> entityList, IDbTransaction transaction = null) where T : class; bool Update<T>(T entity, IDbTransaction transaction = null) where T : class; bool UpdateBatch<T>(IEnumerable<T> entityList, IDbTransaction transaction = null) where T : class; int Delete<T>(dynamic primaryId, IDbTransaction transaction = null) where T : class; int DeleteList<T>(object predicate, IDbTransaction transaction = null) where T : class; bool DeleteBatch<T>(IEnumerable<dynamic> ids, IDbTransaction transaction = null) where T : class;
}

}

View Code

  IDataRepository.cs(提供数据层使用,继承了上面的IDataServiceRepository,支持传入sql)

using System; using System.Collections.Generic; using System.Data; using Dapper; using HY.DataAccess; namespace HY.ORM
{ public interface IDataRepository : IDataServiceRepository
{
IDBSession DBSession { get; }

    IEnumerable<T> Get<T>(string sql, dynamic param = null, bool buffered = true) where T : class;
    IEnumerable<dynamic> Get(string sql, dynamic param = null, bool buffered = true);
    IEnumerable<TReturn> Get<TFirst, TSecond, TReturn>(string sql, Func<TFirst, TSecond, TReturn> map,
        dynamic param \= null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null);

     IEnumerable<TReturn> Get<TFirst, TSecond,TThird, TReturn>(string sql, Func<TFirst, TSecond,TThird, TReturn> map,
        dynamic param \= null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null);

    SqlMapper.GridReader GetMultiple(string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null);


    IEnumerable<T> GetPage<T>(int pageIndex, int pageSize, out long allRowsCount, string sql, dynamic param = null, string allRowsCountSql=null,  dynamic allRowsCountParam = null, bool buffered = true) where T : class;
  
    Int32 Execute(string sql, dynamic param = null, IDbTransaction transaction = null);


}

}

View Code

  RepositoryServiceBase.cs(IDataServiceRepository的实现类)

using System.Collections.Generic; using System.Data; using System.Linq; using Dapper; using DapperExtensions; using HY.DataAccess; namespace HY.ORM
{ public class RepositoryServiceBase : IDataServiceRepository
{ public RepositoryServiceBase()
{
} public RepositoryServiceBase(IDBSession dbSession)
{
DBSession = dbSession;
} public IDBSession DBSession { get; private set; } public void SetDBSession(IDBSession dbSession)
{
DBSession = dbSession;
} ///


/// 根据Id获取实体 ///

///
///
///
public T GetById(dynamic primaryId) where T : class { return DBSession.Connection.Get(primaryId as object, databaseType: DBSession.DatabaseType);
} ///
/// 根据多个Id获取多个实体 ///

///
///
///
public IEnumerable GetByIds(IList ids) where T : class { var tblName = string.Format(“dbo.{0}”, typeof(T).Name); var idsin = string.Join(“,”, ids.ToArray()); var sql = “SELECT * FROM @table WHERE Id in (@ids)”;
IEnumerable dataList = DBSession.Connection.Query(sql, new { table = tblName, ids = idsin }); return dataList;
} ///
/// 获取全部数据集合 ///

///
///
public IEnumerable GetAll() where T : class { return DBSession.Connection.GetList(databaseType: DBSession.DatabaseType);
} ///
/// 统计记录总数 ///

///
///
///
///
public int Count(object predicate, bool buffered = false) where T : class { return DBSession.Connection.Count(predicate, databaseType: DBSession.DatabaseType);
} ///
/// 查询列表数据 ///

///
///
///
///
///
public IEnumerable GetList(object predicate = null, IList sort = null, bool buffered = false) where T : class { return DBSession.Connection.GetList(predicate, sort, null, null, buffered, databaseType: DBSession.DatabaseType);
} ///
/// 分页 ///

///
///
///
///
///
///
///
///
public IEnumerable GetPageList(int pageIndex, int pageSize, out long allRowsCount, object predicate = null, IList sort = null, bool buffered = true) where T : class { if (sort == null)
{
sort = new List();
}
IEnumerable entityList = DBSession.Connection.GetPage(predicate, sort, pageIndex, pageSize, null, null, buffered, databaseType: DBSession.DatabaseType);
allRowsCount = DBSession.Connection.Count(predicate, databaseType: DBSession.DatabaseType); return entityList;
} ///
/// 插入单条记录 ///

///
///
///
///
public dynamic Insert(T entity, IDbTransaction transaction = null) where T : class {
dynamic result = DBSession.Connection.Insert(entity, transaction, databaseType: DBSession.DatabaseType); return result;
} ///
/// 更新单条记录 ///

///
///
///
///
public bool Update(T entity, IDbTransaction transaction = null) where T : class { bool isOk = DBSession.Connection.Update(entity, transaction, databaseType: DBSession.DatabaseType); return isOk;
} ///
/// 删除单条记录 ///

///
///
///
///
public int Delete(dynamic primaryId, IDbTransaction transaction = null) where T : class { var entity = GetById(primaryId); var obj = entity as T; int isOk = DBSession.Connection.Delete(obj, databaseType: DBSession.DatabaseType); return isOk;
} ///
/// 删除单条记录 ///

///
///
///
///
public int DeleteList(object predicate = null, IDbTransaction transaction = null) where T : class { return DBSession.Connection.Delete(predicate, transaction, databaseType: DBSession.DatabaseType);
} ///
/// 批量插入功能 ///

///
///
///
public bool InsertBatch(IEnumerable entityList, IDbTransaction transaction = null) where T : class { bool isOk = false; foreach (var item in entityList)
{
Insert(item, transaction);
}
isOk = true; return isOk;
} ///
/// 批量更新() ///

///
///
///
///
public bool UpdateBatch(IEnumerable entityList, IDbTransaction transaction = null) where T : class { bool isOk = false; foreach (var item in entityList)
{
Update(item, transaction);
}
isOk = true; return isOk;
} ///
/// 批量删除 ///

///
///
///
///
public bool DeleteBatch(IEnumerable ids, IDbTransaction transaction = null) where T : class { bool isOk = false; foreach (var id in ids)
{
Delete(id, transaction);
}
isOk = true; return isOk;
}

}

}

View Code

 RepositoryBase.cs(IDataRepository的实现类)

using System; using System.Collections.Generic; using System.Data; using Dapper; using DapperExtensions; using HY.DataAccess; namespace HY.ORM
{ ///


/// Repository基类 ///

public class RepositoryBase : RepositoryServiceBase, IDataRepository
{ public RepositoryBase()
{
} public new void SetDBSession(IDBSession dbSession)
{ base.SetDBSession(dbSession);
} public RepositoryBase(IDBSession dbSession)
: base(dbSession)
{
} ///
/// 根据条件筛选出数据集合 ///

///
///
///
///
///
public IEnumerable Get(string sql, dynamic param = null, bool buffered = true) where T : class { return DBSession.Connection.Query(sql, param as object, DBSession.Transaction, buffered);
} ///
/// 根据条件筛选数据集合 ///

///
///
///
///
public IEnumerable Get(string sql, dynamic param = null, bool buffered = true)
{ return DBSession.Connection.Query(sql, param as object, DBSession.Transaction, buffered);
} ///
/// 分页查询 ///

///
///
///
///
///
///
///
///
///
///
public IEnumerable GetPage(int pageIndex, int pageSize, out long allRowsCount, string sql, dynamic param = null, string allRowsCountSql = null, dynamic allRowsCountParam = null, bool buffered = true) where T : class {
IEnumerable entityList = DBSession.Connection.GetPage(pageIndex, pageSize, out allRowsCount, sql, param as object, allRowsCountSql, null, null, buffered, databaseType: DBSession.DatabaseType); return entityList;
} ///
/// 根据表达式筛选 ///

///
///
///
///
///
///
///
///
///
///
///
public IEnumerable Get<TFirst, TSecond, TReturn>(string sql, Func<TFirst, TSecond, TReturn> map,
dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = “Id”, int? commandTimeout = null)
{ return DBSession.Connection.Query(sql, map, param as object, transaction, buffered, splitOn);
} ///
/// 根据表达式筛选 ///

///
///
///
///
///
///
///
///
///
///
///
public IEnumerable Get<TFirst, TSecond, TThird, TReturn>(string sql, Func<TFirst, TSecond, TThird, TReturn> map,
dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = “Id”, int? commandTimeout = null)
{ return DBSession.Connection.Query(sql, map, param as object, transaction, buffered, splitOn);
} ///
/// 获取多实体集合 ///

///
///
///
///
///
///
public SqlMapper.GridReader GetMultiple(string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{ return DBSession.Connection.QueryMultiple(sql, param as object, transaction, commandTimeout, commandType);
} ///
/// 执行sql操作 ///

///
///
///
public int Execute(string sql, dynamic param = null, IDbTransaction transaction = null)
{ return DBSession.Connection.Execute(sql, param as object, transaction);
}

}

}

View Code

 说起DapperExtensions修改的小地方还蛮多的,下图是一个代码比较的截图。所以一会把代码打包贴上来吧(见文章结尾)。

 上述代码就可以编译成 HY.ORM.DLL文件了。

下面就可以在 自己业务层继承HY.ORM中的RepositoryServiceBase类   ,数据层继承HY.ORM中的 RepositoryBase类。

通过各自的构造函数或者, SetDBSession(Helper.CreateDBSession());  进行数据连接初始化。

接下来配置实体类和DB的映射:

复制代码

public class DemoEntity
{ public int ID { get; set; } public string Name { get; set; }
}

\[Serializable\] public class DomoEntityORMMapper : ClassMapper<DemoEntity> { public DomoEntityORMMapper()
    { base.Table("Demo"); //Map(f => f.UserID).Ignore();//设置忽略 //Map(f => f.Name).Key(KeyType.Identity);//设置主键  (如果主键名称不包含字母“ID”,请设置) 

AutoMap();
}
}

复制代码

这样就可以在类中  实现   this.Get(“select * from  Demo where ID=@ID”, new { ID = 1 });   这样的语法了。

具体的使用方发

      下图是我要介绍实现的项目截图:

其实也是三层,只是名字不一样而已。  

HY.Web( UI层,MVC)

HY.Web.Iservice( 服务接口层)

HY.Web.Service(服务层,HY.Web.Iservice的实现类,    你也可以理解为业务逻辑层BLL)

HY.Web.DAO(数据访问层,   你也可以理解为DAL)

HY.Web.Entity(实体层,  目前只定义了数据实体, 如果你的系统需要给app提供数据, 那么传输的数据要精简,就需要单独定义DTO了。   )

就那用户表来做个实例吧,表结构如下:(下图是用代码生成器截图效果,可以直接修改数据库的描述信息,开发利器。需要的朋友点这里【CodeBuilder-RazorEngine】

HY.Web.Entity

在HY.Web.Entity的项目中新建Sys_UsersEntity.cs  定义实体类

View Code

HY.Web.DAO

定义基类 BaseRepository.cs (可以设置默认的DBsession,方便扩展其它东东)

View Code

定义数据访问层 Sys_UsersRepository.cs (代码里可以封装任何需要写sql 的代码)

View Code

HY.Web.IService

定义接口 ISys_UsersService.cs ,提供给UI访问。

View Code

HY.Web.Service

定义BaseService.cs,(可以设置默认的DBsession,方便扩展其它东东)

View Code

定义Sys_UsersService.cs, 去实现ISys_UsersService。

View Code

HY.Web

1、定义相关的Controller

2、ISys_UsersService iSys_UsersService = new Sys_UsersService();  (这块其实可以使用 IoC, 相关内容且听后续分解)

3、调用接口

View Code

下载:

HY.DataAccess

修改后的DapperExtensions:**Dapperextensions.RAR**

_ps:已经更新版本了,  加入了对lambda的扩展,点击这里进入

_

相关文章:

搭建一套自己实用的.net架构(1)【概述】

搭建一套自己实用的.net架构(2)【日志模块-log4net】

搭建一套自己实用的.net架构(3)【ORM-Dapper+DapperExtensions】

搭建一套自己实用的.net架构(3)续 【ORM Dapper+DapperExtensions+Lambda】

 搭建一套自己实用的.net架构(4)【CodeBuilder-RazorEngine】

原文链接:http://www.cnblogs.com/hy59005271/p/4759623.html

搭建基于MongoDB的文件管理系统(一) - 作业部落 Cmd Markdown 编辑阅读器

Excerpt

Cmd Markdown 编辑阅读器,支持实时同步预览,区分写作和阅读模式,支持在线存储,分享文稿网址。


MongoDB 文件管理 .NET


1.MongoDB 介绍

Mongodb官网

官网最新版本 V3.2.0,不推荐使用3.0之前的版本

2.基础Shell 操作(备忘)

mongod
启动本地mongo服务,无任何参数,默认会要求在当前目录的根目录下存在data/db目录,用于存放文件

mongo
默认会连接到已经启动的实例,配合上面的命令,可以直接连接到已经启动的服务

mongo 127.0.0.1:27017/myDB
这里可以连接到网络上的mongodb服务的 myDB数据库,只能能够访问,一般部署在安全的内网环境
通过 –nodb 参数可以不连接到任何mongod服务

drop
命令速度非常快,但是需要重新建立索引

3. C#驱动操作MongoDB数据库,代码示例(GridFS)

@链接mongodb服务,设置当前数据库为 testDB

1
2
var client = new MongoClient("mongodb://127.0.0.1:27017");
var database = client.GetDatabase("testDB");

@设置 GridFS

1
var fs = new GridFSBucket(database);

@上传 文件流到数据库,GridFS,此时应该是同步的操作,异步操作暂时没学习 ^_^

1
2
3
4
5
6
7
8
9
10
11
private static ObjectId UploadFile(GridFSBucket fs)
{
using (var s = File.OpenRead(@"C:\Pictures\Q.png"))
{
var t = Task.Run&lt;ObjectId&gt;(() =&gt;
{
return fs.UploadFromStreamAsync(s.Name, s);
});
return t.Result; //返回值是mongodb的ObjectId类型,可以直接.toString转换成字符串进行持久化
}
}

@从MongoDB下载文件
@这里没有进行判断文件是否存在,直接使用ObjectId进行查找,然后下载,TODO 做异常捕获

1
2
3
4
5
6
private static void DownloadFile(GridFSBucket fs, ObjectId id)
{
var t = fs.DownloadAsBytesAsync(id);
Task.WaitAll(t);
var bytes = t.Result;
}

@上传JSON格式的数据到mongoDB,并且可以主动新增属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static ObjectId UploadJsonToMongoDB(string json, Dictionary&lt;string, string&gt; otherKv, string _database, string _collection)
{
MongoDB.Bson.BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize&lt;MongoDB.Bson.BsonDocument&gt;(json);
ObjectId oId = ObjectId.GenerateNewId();
document.Add("_id", oId);//添加ObjectId,一般使用驱动设置,如果不指定,保存到数据库时会自动添加
foreach (var kv in otherKv)
{
document.Add(kv.Key, kv.Value);
}
var database = client.GetDatabase(_database);
var collection = database.GetCollection&lt;BsonDocument&gt;(_collection);
collection.InsertOneAsync(document);
return oId;
}

@从mongoDB中下载json格式的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static string GetJsonFromMongoDB(string objid, FilterDefinition&lt;BsonDocument&gt; _filter, string _database, string _collection)
{
var database = client.GetDatabase(_database);
var collection = database.GetCollection&lt;BsonDocument&gt;(_collection);
FilterDefinition&lt;BsonDocument&gt; filter = null;
if (!string.IsNullOrEmpty(objid))
{
ObjectId oId = new ObjectId(objid);
filter = Builders&lt;BsonDocument&gt;.Filter.Eq("_id", oId);
}
else if (_filter != null)
{
filter = _filter;
}
var documents = collection.Find(filter).ToListAsync();
var t = Task.Run(() =&gt;
{
return collection.Find(filter).ToListAsync();
});
if (t.Result.Count &gt; 0)
{
var jsonWriterSettings = new MongoDB.Bson.IO.JsonWriterSettings { OutputMode = MongoDB.Bson.IO.JsonOutputMode.Strict };
//这里的OutputMode是为了输出可以直接被js接受的json数据
return t.Result[0].ToJson(jsonWriterSettings);
}
else
{
return "";
}
}

抄自《更改MySQL数据库的编码为utf8mb4》。

utf-8 编码可能2个字节、3个字节、4个字节的字符,但是 MySQLutf8 编码只支持3字节的数据,而移动端的表情数据是4个字节的字符。如果直接往采用 utf-8 编码的数据库中插入表情数据,Java 程序中将报SQL异常:

java.sql.SQLException: Incorrect string value: ‘\xF0\x9F\x92\x94’ for column ‘name’ at row 1
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1073)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3593)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3525)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1986)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2140)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2620)
at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1662)
at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1581)

可以对4字节的字符进行编码存储,然后取出来的时候,再进行解码。但是这样做会使得任何使用该字符的地方都要进行编码与解码。

utf8mb4编码是utf8编码的超集,兼容utf8,并且能存储4字节的表情字符。
采用utf8mb4编码的好处是:存储与获取数据的时候,不用再考虑表情字符的编码与解码问题。

更改数据库的编码为utf8mb4:

1. MySQL的版本

utf8mb4 的最低 MySQL 版本支持版本为 **5.5.3+**,若不是,请升级到较新版本。

2. MySQL驱动

5.1.34 可用,最低不能低于 5.1.13

3. 修改MySQL配置文件

修改 MySQL 配置文件 _my.cnf_(windows为_my.ini_)

my.cnf一般在etc/mysql/my.cnf位置。找到后请在以下三部分里添加如下内容: 
[client] 
default-character-set = utf8mb4 
[mysql] 
default-character-set = utf8mb4 
[mysqld] 
character-set-client-handshake = FALSE 
character-set-server = utf8mb4 
collation-server = utf8mb4_unicode_ci 
init_connect='SET NAMES utf8mb4'

4. 重启数据库,检查变量

SHOW VARIABLES WHERE Variable_name LIKE 'character_set_%' OR Variable_name LIKE 'collation%';
Variable_name Value
character_set_client utf8mb4
character_set_connection utf8mb4
character_set_database utf8mb4
character_set_filesystem binary
character_set_results utf8mb4
character_set_server utf8mb4
character_set_system utf8
collation_connection utf8mb4_unicode_ci
collation_database utf8mb4_unicode_ci
collation_server utf8mb4_unicode_ci

collation_connectioncollation_databasecollation_server是什么没关系,但必须保证:

系统变量

描述

character_set_client

(客户端来源数据使用的字符集)

character_set_connection

(连接层字符集)

character_set_database

(当前选中数据库的默认字符集)

character_set_results

(查询结果字符集)

character_set_server

(默认的内部操作字符集)

这几个变量必须是utf8mb4

5. 数据库连接的配置

数据库连接参数中:
characterEncoding=utf8 会被自动识别为 utf8mb4,也可以不加这个参数,会自动检测。
autoReconnect=true 是必须加上的。

6. 将数据库和已经建好的表也转换成utf8mb4

修改数据库字符集:

1
2
3
ALTER DATABASE db_name DEFAULT CHARACTER SET character_name [COLLATE ...];
--示例:
ALTER DATABASE test DEFAULT CHARACTER SET utf8mb4;

把表默认的字符集和所有字符列改为新的字符集:

1
2
3
ALTER TABLE tbl_name CONVERT TO CHARACTER SET character_name [COLLATE ...]
--示例:
ALTER TABLE logtest CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8_general_ci;

只是修改表的默认字符集:

1
2
3
ALTER TABLE tbl_name DEFAULT CHARACTER SET character_name [COLLATE...];
--示例:
ALTER TABLE logtest DEFAULT CHARACTER SET utf8mb4 COLLATE utf8_general_ci;

修改字段的字符集:

1
2
3
ALTER TABLE tbl_name CHANGE c_name c_name CHARACTER SET character_name [COLLATE ...];
--示例:
ALTER TABLE logtest CHANGE title title VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci;

查看数据库编码:

1
SHOW CREATE DATABASE db_name;

查看表编码:

1
SHOW CREATE TABLE tbl_name;

查看字段编码:

1
SHOW FULL COLUMNS FROM tbl_name;

查看当前数据库编码:

1
SHOW VARIABLES LIKE 'character_set_%';

mysql-connector-java驱动包在5.1.13+才支持utf8mb4

1
2
3
4
set global character_set_server=utf8mb4;
set global character_set_database=utf8mb4;
set global character_set_client=utf8mb4;
set global character_set_connection=utf8mb4;

修改配置文件:

1
2
3
4
5
6
--修改mysql的my.cnf文件中的字符集
--如:default-character-set = utf8mb4
character_set_server = utf8mb4
character-set-client-handshake = FALSE
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'

大学四年,即将毕业!

大学期间的最后一篇博客,总结分享下我做的毕业设计。我选的论文命题为《燃气管网设备仪器进销存管理系统之后台设计》,由于我们专业只有我一个走技术路线,所以,我一个人完成了整个系统的设计及开发,总耗时近一个月,最终获得优的成绩。

这里不讨论论文,不写具体实现细节,主要讲如何一步步搭建自己的系统框架及系统实现,分享下自己的心得,新手可以互相学习,大牛们就当看作本人抛砖引玉啦!!

博客最后会附上系统开发相关的所有文件但不包括毕业论文,本文已大体包含了论文的内容!

一、系统展示

1.登录页面

  

2.admin登录后的主页

  

3.菜单管理

  

4.角色管理>新增角色

  

5.用户管理

  

6.添加商品

  

7.仓库管理

  

8.供应商管理

  

9.采购订单管理

  

10.采购订单导出

  

11.库存查看

  

12.采购统计

  

二、系统需求分析

  上面简单的展示了完成后的系统测试截图,你可以下载war包部署到自己的tomcat上看,下面开始进入正文。

  开发一个(简单)系统,我想首要做的就是进行系统需求分析,弄清楚你为什么要做这个系统,这个系统有哪些功能模块,每个功能具体实现些什么功能。

  当然,我这里的主要目的是完成毕业设计,交出毕业论文。但我并没有简单的只是为了完成一个毕业设计而去开发这个系统。主要想法是以开发这套进销存管理系统为例,详细说明一个系统从确认需求、技术选型、架构设计、系统实现到测试部署的整个开发过程。综合运用自己平时所学的知识、技术,及实习获得的经验等,去完整且较好的实现一个系统。搭建一个基础系统框架,形成一定规范,以后在此基础上做开发,可以省去很多诸如搭建框架、加入依赖、配置等工作。

  这次开发所做的主要工作及意义如下:   

    ①学会站在用户的角度分析用户需求,完成需求分析设计等。

    ②熟练使用各种类相关开发、设计工具,及开源软件。

    ③熟练掌握Spring+SpringMVC+Hibernate+ExtJs的开发技术。

    ④熟练使用maven构建工具。

    ⑤站在企业的角度,试着搭建自己的一个底层基础框架。

    ⑥建立完整的燃气管进销存管理系统,进行测试并分析结果。

    ⑦将系统部署到互联网上,以实现真正的web应用。

1.问题分析

  首先,对系统进行需求分析,首先需要了解的就是什么是进销存系统,进销存系统也称为供应链管理系统,最基本的内容就是采购、库存、销售、退货管理。进销存系统是对企业生产经营中采购、入库、销售进行跟踪管理,从采购单开始,到商品入库,商品销售出库,每一步都跟踪记录。有效解决企业的分销管理等业务问题。那么燃气管进销存系统有何不同呢,其实进销存系统已经具备了一般商品的进销存功能,燃气管就是一种商品。以此为出发点,开始着手设计系统功能模块。

2.系统模块结构

  系统分为6大模块,分别是基础设置、采购管理、销售管理、库存管理、统计分析、系统管理。系统设置的角色有admin、采购员、销售员、库存管理员等。

  系统模块结构(使用xmind设计):

  

3.系统总体流程

  系统的一个整体流程,从初次使用开始,系统设置一个超级管理员(admin),拥有系统的所有权限。admin登录系统,设置角色(系统管理员,采购员,销售员,库存管理员),分配对应的权限。然后进入用户管理,录入系统用户。一般管理员登录系统,录入基础数据;采购员需要录入供应商,采购单,退货单;销售员则需录入销售单,退货单。库存管理员就需要在库存管理中进行采购审核和销售审核,采购审核通过则商品入库,增加库存;销售审核通过则商品出库,减少库存。管理员还可以查看每月的采购统计和销售统计。

  系统总体流程(使用visio设计):

  

三、开发环境简介

  需求确定了,就要进入具体的开发阶段,首先确定开发这个系统综合用到哪些技术、开发工具等。

  简单说下这个系统的开发环境:   

    开发平台:windows 8.1

    Java版本:jdk 1.8

    项目管理工具:Maven

    开发工具:Intellij IDEA

    数据库:MySql 5.1

    服务器:Tomcat 8.5

    开发框架:Spring4 + SpringMVC + Hibernate5

    前端框架:ExtJs 4.2 + Jsp

    建模工具:PowerDesigner、Visio

1.maven

  Maven是Apache软件基金会组织维护的一款自动化构建工具,专注服务于Java平台的项目构建和依赖管理。它提供了中央仓库,能帮我们自动下载构件和第三方的开源类库。你只需要在你的项目中以坐标的方式依赖一个jar包,maven就会自动从中央仓库下载,并同时下载这个jar包所依赖的其他jar包,以及可以下载源码进行阅读。使用maven后每个jar包本身只在本地仓库中保存一份,极大的节约了存储空间,让项目更轻巧,更避免了重复文件太多而造成的混乱。同时maven可以替我们自动的将当前jar包所依赖的其他所有jar包全部导入进来,无需人工参与,节约了大量的时间和精力。使用maven,只需要一条简单的命令,就可以自动完成清理、编译、测试、打包、部署的整个过程。我们的项目一般会分为开发环境和生产环境,不同环境对应不同的配置文件,使用maven,你就可以配置两个环境,打包的时候指定运行的环境,就可以将对应的配置文件替换,以此减少手工操作及可能带来的失误操作等。

2.Intellij IDEA

  IDEA是java语言开发的集成环境,Intellij被公认为最好的Java开发工具之一。IDEA在代码自动提示、重构、调试、各类版本工具(maven、svn等)整合等方面都是比较强的。本人是在实习期间转用idea开发的,之前一直使用eclipse,相比eclipse,idea在调试、代码自动提示等方面更显优势。项目在idea中有一个更友好的目录结构,尤其是多工程项目。当然,eclipse比idea更容易上手,使用idea可以提高你的开发速度,但前提是你需要记住大量的快捷键。使用idea的调试功能,比如,你只需要按快捷键Alt+F8,然后输入表达式,就可以快速求值;在调试的时候,idea会在变量的后面以不同的颜色显示变量的值,你就可以很清楚的知道调试的每一步,非常方便。使用好IDEA能在很大程度上提高我们的开发速度。

3.ExtJs

  ExtJs可以用来开发富客户端的ajax应用,是用javascript写的与后台技术无关的前端ajax框架,主要用于创建前端用户界面,拥有强大的数据处理功能,以及图表统计等。同时,ExtJs拥有很多个性化的主题供你选择,是开发后台管理系统的一个不错的选择。

四、底层架构设计

  在进行进销存系统的设计和编码之前,首先设计一个自己的底层框架,这个底层框架在之后可以作为其它具体项目开发的一个基础,从而不必每次开发项目时,都去做很多重复的工作。这个底层框架主要包括一个开发的规范,以及一些通用的工具类等,更重要的是分类别引入各个框架,如Spring、Hibernate、各个配置文件等。同时,如果以后在开发中,增加的一些新功能,还可以往这个底层中添加,不断的去完善。

1.规范

  在进行框架设计之前,为了使软件开发过程顺畅、提高代码的可靠性,可读性和可维护性等,首先需要确定的就是开发规范了,俗话说,没有规矩不成方圆,软件开发亦是如此。下面列出一些简单的需要遵守的规范。

1.1基础规范

  首先需要遵守的是一些基础规范。一般来说,公司会将域名作为所有命名的一个基础,比如文件名、包名等等。因此我申请了一个域名[www.lyyzoo.com\]作为个人域名。然后将D:/lyyzoo-repo作为开发的根目录,即个人代码仓库,以后所有的项目都会建到这个目录下。所有的项目开发使用maven来管理项目,因此目录结构是标准的maven规范目录。

  maven约定的目录结构:

  

1.2代码规范

  ①命名  

    > 所有的命名需要见名之意,尽量保证通过变量名得知变量的含义,需要注释的地方尽量添加注释。

    > 包命名全小写,通过域名倒写+模块的形式,如:com.lyyzoo.service

    > 类命名采用Pascal名法,大写字母开头,每个单词首字母大写。

    > 方法名采用Camel命名法,小写字母开头,每个单词首字母小写;getter和setter使用Lombok自动生成,只需添加@Data注解即可。

    > 变量名采用Camel命名法,小写字母开头,每个单词首字母大写。变量名不宜过长,可采用首字母缩写的形式,但要见名之意。

    > 常量名全大写,每个单词之间使用”_”分隔。

  ②分层

    项目以功能模块划分,不同项目建立不同的工程,使用maven的依赖进行管理。包的基本分层有controller(控制层)、service(业务层)、dao(数据访问层)、entity(模型层)。

2.架构设计

2.1模块结构

  整个项目的底层着重是一些通用的、基础的东西,整合到一起,以便于以后重用。首先,创建一个名为lyyzoo的maven工程,lyyzoo将作为底层的根目录。lyyzoo下有两个主要的子模块,分别为lyyzoo-base和lyyzoo-starter,lyyzoo-base是基础模块,用于一些简单的Java及JavaEE程序;lyyzoo-starter则是JavaEE相关,会依赖于lyyzoo-base,同时引入了Spring、Hibernate等第三方框架。然后在各个模块中添加具体的子模块。以后开发中需要用到哪个模块,在依赖中添加那个模块即可。

  底层模块结构图:

  

  以下是各个POM之间的关系:

  ① lyyzoo > pom.xml

  

  ② lyyzoo-base > pom.xml

  

  ③ lyyzoo-starter > pom.xml

  

2.2依赖管理

  结构建好后,就需要进行一些详细的依赖配置工作了,lyyzoo是所有模块的父类,所以在lyyzoo中需要添加公用的属性、依赖管理、maven插件等。

  首先将所有的版本号提出来,放到里,这样一旦需要切换到另一个版本时,就可以只改个版本号就达到目的了。其中列出了一些属性如下:包括底层的版本、Java版本、Spring、Hibernate的版本等等。

1 <properties>
2
3 <lyyzoo.version>1.0-SNAPSHOT</lyyzoo.version>
4 <java.version>1.8</java.version>
5 <maven.compiler.source>${java.version}</maven.compiler.source>
6 <maven.compiler.target>${java.version}</maven.compiler.target>
7 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
8
9 <lombok.version>1.16.14</lombok.version>
10
11 <slf4j.version>1.7.7</slf4j.version>
12 <logback.version>1.1.3</logback.version>
13
14 <mysql.version>5.1.38</mysql.version>
15
16 <c3p0.version>0.9.5.2</c3p0.version>
17
18 <junit.version>4.12</junit.version>
19
20 <servlet.version>3.1.0</servlet.version>
21
22 <spring.version>4.2.6.RELEASE</spring.version>
23 <aspectjrt.version>1.7.3</aspectjrt.version>
24 <aspectjweaver.version>1.7.3</aspectjweaver.version>
25
26 <hibernate.version>5.0.1.Final</hibernate.version>
27 <hibernate.jpa.version>1.0.0.Final</hibernate.jpa.version>
28 </properties>

View Code

  接着,引入依赖管理,在lyyzoo中引入其它项目将会用到的所有三方jar包的依赖,所有的依赖都添加到中,这样就可以方便的管理所有的jar包了。下面列出引入的一部分jar包,其它的可参考源码。

  ① 首先需要引入lyyzoo下的其它模块,如lyyzoo-base-core、lyyzoo-starter-base等模块。

1
2 <dependency>
3 <groupId>com.lyyzoo</groupId>
4 <artifactId>lyyzoo-base-core</artifactId>
5 <version>${lyyzoo.version}</version>
6 </dependency>
7 <dependency>
8 <groupId>com.lyyzoo</groupId>
9 <artifactId>lyyzoo-base-data</artifactId>
10 <version>${lyyzoo.version}</version>
11 </dependency>
12 <dependency>
13 <groupId>com.lyyzoo</groupId>
14 <artifactId>lyyzoo-starter-base</artifactId>
15 <version>${lyyzoo.version}</version>
16 </dependency>
17 <dependency>
18 <groupId>com.lyyzoo</groupId>
19 <artifactId>lyyzoo-starter-jpa</artifactId>
20 <version>${lyyzoo.version}</version>
21 </dependency>

View Code

  ② JDBC相关,相关jar包有c3p0,用于作数据库连接池;mysql驱动包;dbutils,对JDBC进行了简单的封装,使用起来简单方便。

1
2 <dependency>
3 <groupId>com.mchange</groupId>
4 <artifactId>c3p0</artifactId>
5 <version>${c3p0.version}</version>
6 </dependency>
7 <dependency>
8 <groupId>mysql</groupId>
9 <artifactId>mysql-connector-java</artifactId>
10 <version>${mysql.version}</version>
11 </dependency>
12 <dependency>
13 <groupId>commons-dbutils</groupId>
14 <artifactId>commons-dbutils</artifactId>
15 <version>1.5</version>
16 </dependency>

View Code

  ③ 日志相关:

1
2 <dependency>
3 <groupId>org.slf4j</groupId>
4 <artifactId>slf4j-api</artifactId>
5 <version>${slf4j.version}</version>
6 </dependency>
7 <dependency>
8 <groupId>ch.qos.logback</groupId>
9 <artifactId>logback-classic</artifactId>
10 <version>${logback.version}</version>
11 </dependency>
12
13 <dependency>
14 <groupId>org.slf4j</groupId>
15 <artifactId>log4j-over-slf4j</artifactId>
16 <version>${slf4j.version}</version>
17 </dependency>

View Code

  ④ spring相关,包括了spring aop、spring mvc等。

1
2 <dependency>
3 <groupId>org.springframework</groupId>
4 <artifactId>spring-aop</artifactId>
5 <version>${spring.version}</version>
6 </dependency>
7 <dependency>
8 <groupId>org.springframework</groupId>
9 <artifactId>spring-aspects</artifactId>
10 <version>${spring.version}</version>
11 </dependency>
12 <dependency>
13 <groupId>org.springframework</groupId>
14 <artifactId>spring-beans</artifactId>
15 <version>${spring.version}</version>
16 </dependency>
17 <dependency>
18 <groupId>org.springframework</groupId>
19 <artifactId>spring-context</artifactId>
20 <version>${spring.version}</version>
21 </dependency>
22 <dependency>
23 <groupId>org.springframework</groupId>
24 <artifactId>spring-context-support</artifactId>
25 <version>${spring.version}</version>
26 </dependency>
27 <dependency>
28 <groupId>org.springframework</groupId>
29 <artifactId>spring-core</artifactId>
30 <version>${spring.version}</version>
31 </dependency>
32 <dependency>
33 <groupId>org.springframework</groupId>
34 <artifactId>spring-jdbc</artifactId>
35 <version>${spring.version}</version>
36 </dependency>
37 <dependency>
38 <groupId>org.springframework</groupId>
39 <artifactId>spring-jms</artifactId>
40 <version>${spring.version}</version>
41 </dependency>
42 <dependency>
43 <groupId>org.springframework</groupId>
44 <artifactId>spring-orm</artifactId>
45 <version>${spring.version}</version>
46 </dependency>
47 <dependency>
48 <groupId>org.springframework</groupId>
49 <artifactId>spring-tx</artifactId>
50 <version>${spring.version}</version>
51 </dependency>
52 <dependency>
53 <groupId>org.springframework</groupId>
54 <artifactId>spring-web</artifactId>
55 <version>${spring.version}</version>
56 </dependency>
57 <dependency>
58 <groupId>org.springframework</groupId>
59 <artifactId>spring-webmvc</artifactId>
60 <version>${spring.version}</version>
61 </dependency>

View Code

  ⑤ hibernate相关:

1 <dependency>
2 <groupId>org.hibernate</groupId>
3 <artifactId>hibernate-c3p0</artifactId>
4 <version>${hibernate.version}</version>
5 </dependency>
6 <dependency>
7 <groupId>org.hibernate.common</groupId>
8 <artifactId>hibernate-commons-annotations</artifactId>
9 <version>${hibernate.version}</version>
10 </dependency>
11 <dependency>
12 <groupId>org.hibernate</groupId>
13 <artifactId>hibernate-core</artifactId>
14 <version>${hibernate.version}</version>
15 </dependency>
16 <dependency>
17 <groupId>org.hibernate</groupId>
18 <artifactId>hibernate-entitymanager</artifactId>
19 <version>${hibernate.version}</version>
20 </dependency>

View Code

  所有的依赖添加好后,就需要为各个子模块添加具体的依赖了,根据每个子模块的功能,添加相关的依赖,而不是将所有的依赖一次性加入。这样我们就可以根据自己开发的项目的需要,添加模块依赖,而不是一次性加入所有jar包,避免冗余,增大项目的体积。下面以lyyzoo-base-data和lyyzoo-starter-jpa为例说明。

  lyyzoo-base-data模块是基础数据相关,主要与数据库打交道,那么就需要引入mysql驱动、数据库连接池c3p0等,pom.xml如下:

1 <project xmlns=“http://maven.apache.org/POM/4.0.0“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
2 xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"\>
3 <modelVersion>4.0.0</modelVersion>
4 <parent>
5 <groupId>com.lyyzoo</groupId>
6 <artifactId>lyyzoo-base</artifactId>
7 <version>1.0-SNAPSHOT</version>
8 </parent>
9 <artifactId>lyyzoo-base-data</artifactId>
10 <name>${project.artifactId}</name>
11 <packaging>jar</packaging>
12
13 <dependencies>
14
15 <dependency>
16 <groupId>commons-dbutils</groupId>
17 <artifactId>commons-dbutils</artifactId>
18 </dependency>
19
20 <dependency>
21 <groupId>mysql</groupId>
22 <artifactId>mysql-connector-java</artifactId>
23 </dependency>
24 <dependency>
25 <groupId>com.mchange</groupId>
26 <artifactId>c3p0</artifactId>
27 </dependency>
28
29 <dependency>
30 <groupId>com.lyyzoo</groupId>
31 <artifactId>lyyzoo-base-test</artifactId>
32 <scope>test</scope>
33 </dependency>
34 </dependencies>
35 </project>

View Code

  lyyzoo-starter-jpa是Java持久化相关,所以主要引入hibernate相关的依赖,同时,starter-jpa会依赖base-data及starter-base,pom.xml如下:

1 <project xmlns=“http://maven.apache.org/POM/4.0.0“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
2 xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"\>
3 <modelVersion>4.0.0</modelVersion>
4 <parent>
5 <groupId>com.lyyzoo</groupId>
6 <artifactId>lyyzoo-starter</artifactId>
7 <version>1.0-SNAPSHOT</version>
8 </parent>
9 <artifactId>lyyzoo-starter-jpa</artifactId>
10 <name>${project.artifactId}</name>
11 <packaging>jar</packaging>
12
13 <dependencies>
14 <dependency>
15 <groupId>com.lyyzoo</groupId>
16 <artifactId>lyyzoo-base-data</artifactId>
17 </dependency>
18 <dependency>
19 <groupId>com.lyyzoo</groupId>
20 <artifactId>lyyzoo-starter-base</artifactId>
21 </dependency>
22 <dependency>
23 <groupId>org.hibernate</groupId>
24 <artifactId>hibernate-c3p0</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.hibernate.common</groupId>
28 <artifactId>hibernate-commons-annotations</artifactId>
29 </dependency>
30 <dependency>
31 <groupId>org.hibernate</groupId>
32 <artifactId>hibernate-core</artifactId>
33 </dependency>
34 <dependency>
35 <groupId>org.hibernate</groupId>
36 <artifactId>hibernate-ehcache</artifactId>
37 </dependency>
38 <dependency>
39 <groupId>org.hibernate.javax.persistence</groupId>
40 <artifactId>hibernate-jpa-2.1-api</artifactId>
41 </dependency>
42 <dependency>
43 <groupId>org.hibernate</groupId>
44 <artifactId>hibernate-validator</artifactId>
45 </dependency>
46 <dependency>
47 <groupId>com.lyyzoo</groupId>
48 <artifactId>lyyzoo-starter-test</artifactId>
49 <scope>test</scope>
50 </dependency>
51 </dependencies>
52 </project>

View Code

2.3类结构

  有了前面的基础之后,接下来进行重点的类结构设计。底层需要做的最重要的工作就是将一些通用的类抽象出来,以便于以后重用,从而提高开发效率。例如返回结果和分页类的封装、通用的工具类、JDBC相关的操作等。

  说明一下,整个底层很大一部分是从之前实习的公司(这个就不说了)直接拿过来的,有些则更改了。相信我拿取的这部分代码并不会涉及机密问题,不用于商业用途,仅仅只是学习,应该没多大问题。

(1) lyyzoo-base-core

  lyyzoo-base-core 目录结构:

  

  lyyzoo-base-core是基础核心,封装了返回结果和加入了一些常用的工具类。

  com.lyyzoo.bean包下封装了BaseBean和Result,BaseBean是Bean类的一个基础类,实现了序列化,重写了toString()方法,如下:

1 package com.lyyzoo.bean; 2
3 import org.apache.commons.lang3.builder.ToStringBuilder; 4 import org.apache.commons.lang3.builder.ToStringStyle; 5
6 import java.io.Serializable; 7
8 /**
9 * BaseBean 实现序列化

10 * 11 * @author bojiangzhou 12 * @date 2017-03-27 13 */
14 public abstract class BaseBean implements Serializable { 15
16 /**
17 * ToStringBuilder – 用于辅助实现Object.toString()方法

18 */
19 public String toString() { 20 return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); 21 } 22
23 }

View Code

  Result则封装了返回的结果,这里主要与ExtJs相对应,ExtJs进行ajax请求时,返回success=true,会进入success()方法;返回success=false,会进入failure()方法。同时,封装了返回消息、标识、错误信息等,便于前端拿到相应数据。

1 package com.lyyzoo.bean; 2
3 import lombok.Data; 4
5 import java.util.Map; 6
7 /**
8 * 封装返回结果


9 * 10 * @author bojiangzhou 11 * @date 2017-03-27 12 */
13 @Data 14 public class Result extends BaseBean { 15 private static final long serialVersionUID = 1L; 16
17 /**
18 * 成功标志 19 */
20 public boolean success; 21 /**
22 * 返回标示 23 */
24 public Integer code; 25 /**
26 * 相关消息 27 */
28 public String msg; 29 /**
30 * 相关数据 31 */
32 public Object data; 33 /**
34 * 错误详细 35 */
36 public Map<String, Object> errors; 37
38 public Result() { 39
40 } 41
42 public Result(boolean success) { 43 this.success = success; 44 } 45
46 public Result(boolean success, Integer code, Object data, String msg) { 47 this(success); 48 this.code = code; 49 this.data = data; 50 this.msg = msg; 51 } 52
53 public Result(Integer code, Map<String, Object> errors, String msg) { 54 this.success = false; 55 this.code = code; 56 this.errors = errors; 57 this.msg = msg; 58 } 59
60 public boolean isSuccess() { 61 return this.success; 62 } 63
64 public boolean hasErrors() { 65 if (this.errors != null && this.errors.size() > 0) { 66 return true; 67 } 68 return false; 69 } 70 }

View Code

  其它的主要是一些工具类,例如Arrays继承了org.apache.commons.lang3.ArrayUtils,使得Arrays具有了丰富的操作集合的工具。再如Dates,Dates里封装了大量的对日期操作的方法,比如格式化日期、获取某个日期当天的开始时间和结束时间等。

(2) lyyzoo-starter-base

  lyyzoo-starter-base 目录结构:

  

  lyyzoo-starter-base是web应用程序的一个基础,主要封装了基础实体类以及spring-base和日志的配置。实体的顶层是AbstractEntity,AbstractEntity继承BaseBean。AbstractEntity重写了equals和hashCode方法,主要做的处理是,如果两个实体的id相同也算这两个对象为同一个对象。代码如下:

1 package com.lyyzoo.data.entity; 2
3 import com.lyyzoo.bean.BaseBean; 4
5 import java.io.Serializable; 6
7 /**
8 * 抽象实体类 重写equals和hashCode方法
9 * 10 * @author bojiangzhou 11 * @date 2017-03-28 12 */
13 @SuppressWarnings(“serial”) 14 public abstract class AbstractEntity extends BaseBean implements Serializable { 15
16 public abstract ID getId(); 17
18 @Override 19 public boolean equals(Object obj) { 20
21 if (null == obj) { 22 return false; 23 } 24 if (this == obj) { 25 return true; 26 } 27 if (!getClass().equals(obj.getClass())) { 28 return false; 29 } 30
31 AbstractEntity that = (AbstractEntity) obj; 32
33 return null == this.getId() ? false : this.getId().equals(that.getId()); 34 } 35
36 @Override 37 public int hashCode() { 38 int hashCode = 17; 39
40 hashCode += null == getId() ? 0 : getId().hashCode() * 31; 41
42 return hashCode; 43 } 44
45 }

View Code

  BaseEntity则继承AbstractEntity,BaseEntity作为其它实体的父类存在,BaseEntity定义了主键ID的生成策略。代码如下:

1 package com.lyyzoo.data.entity; 2
3 import lombok.Data; 4
5 import javax.persistence.GeneratedValue; 6 import javax.persistence.GenerationType; 7 import javax.persistence.Id; 8 import javax.persistence.MappedSuperclass; 9
10 /**
11 * 统一定义id的entity基类. 12 *

13 * 抽象实体基类,提供统一的ID,和相关的基本功能 14 *

15 */
16 @Data 17 @MappedSuperclass 18 @SuppressWarnings(“serial”) 19 public abstract class BaseEntity extends AbstractEntity { 20
21 /**
22 * id 字段 23 */
24 @Id 25 protected Long id; 26
27 @Id 28 @GeneratedValue(strategy = GenerationType.IDENTITY) 29 public Long getId() { 30 return this.id; 31 } 32
33 public void setId(Long id) { 34 this.id = id; 35 } 36 }

View Code

  spring-base.xml是基础配置,主要配置了扫描系统的配置文件、注解等。

1
2 <beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
3 xmlns=“http://www.springframework.org/schema/beans
4 xmlns:beans=“http://www.springframework.org/schema/beans
5 xmlns:context=“http://www.springframework.org/schema/context
6 xmlns:aop=“http://www.springframework.org/schema/aop
7 xmlns:tx=“http://www.springframework.org/schema/tx
8 xmlns:util=“http://www.springframework.org/schema/util
9 xmlns:mvc=“http://www.springframework.org/schema/mvc
10 xmlns:jee=“http://www.springframework.org/schema/jee
11 xmlns:jdbc=“http://www.springframework.org/schema/jdbc
12 xmlns:tool=“http://www.springframework.org/schema/tool
13 xmlns:dwr=“http://www.directwebremoting.org/schema/spring-dwr
14 xsi:schemaLocation=“ 15 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 16 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 17 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd 18 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd 19 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd 20 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd 21 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd 22 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd 23 http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd 24 http://www.directwebremoting.org/schema/spring-dwr http://www.directwebremoting.org/schema/spring-dwr-3.0.xsd
25 >
26
27 <description>Spring Base Config</description>
28
29
30 <context:property-placeholder location=“classpath*:/config/*.properties” file-encoding=“UTF-8” />
31
32
33 <context:component-scan base-package=“com.lyyzoo”>
34 <context:exclude-filter type=“annotation” expression=“org.springframework.stereotype.Controller”/>
35 <context:exclude-filter type=“annotation” expression=“org.springframework.web.bind.annotation.ControllerAdvice”/>
36 </context:component-scan>
37
38
39 <bean id=“multipartResolver” class=“org.springframework.web.multipart.commons.CommonsMultipartResolver”/>
40
41 </beans>

View Code

(3) lyyzoo-starter-jpa

lyyzoo-starter-jpa 目录结构:

  lyyzoo-starter-jpa集成了持久化相关的操作,配置等。

  首先需要做持久化相关的配置(spring-base-jpa.xml),如数据源、SessionFactory、事务管理等。数据源使用c3p0,数据源相关配置如数据库驱动、地址等写到到配置文件中。配置Hibernate SessionFactory的同时,增加了JdbcTemplate。事务管理器使用Hibernate的事务管理。总的配置如下:

1
2 <beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
3 xmlns=“http://www.springframework.org/schema/beans
4 xmlns:beans=“http://www.springframework.org/schema/beans
5 xmlns:tx=“http://www.springframework.org/schema/tx
6 xmlns:aop=“http://www.springframework.org/schema/aop
7 xmlns:context=“http://www.springframework.org/schema/context
8 xmlns:mvc=“http://www.springframework.org/schema/mvc
9 xmlns:util=“http://www.springframework.org/schema/util
10 xmlns:jee=“http://www.springframework.org/schema/jee
11 xmlns:jdbc=“http://www.springframework.org/schema/jdbc
12 xmlns:tool=“http://www.springframework.org/schema/tool
13 xmlns:dwr=“http://www.directwebremoting.org/schema/spring-dwr
14 xsi:schemaLocation=“
15 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
16 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
17 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
18 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
19 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
20 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
21 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
22 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
23 http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd
24 http://www.directwebremoting.org/schema/spring-dwr http://www.directwebremoting.org/schema/spring-dwr-3.0.xsd
25 >
26
27 <description>Spring Jpa</description>
28
29
30 <bean id=“dataSource” class=“com.mchange.v2.c3p0.ComboPooledDataSource”>
31 <property name=“driverClass” value=“${jdbc.driver}”/>
32 <property name=“jdbcUrl” value=“${jdbc.url}”/>
33 <property name=“user” value=“${jdbc.username}”/>
34 <property name=“password” value=“${jdbc.password}”/>
35
36
37
38 <property name=“initialPoolSize” value=“${c3p0.initialPoolSize:5}”/>
39
40
41
42 <property name=“minPoolSize” value=“${c3p0.minPoolSize:5}”/>
43
44
45
46 <property name=“maxPoolSize” value=“${c3p0.maxPoolSize:15}”/>
47
48
49
50 <property name=“maxIdleTime” value=“${c3p0.maxIdleTime:600}”/>
51 </bean>
52
53
54 <bean id=“sessionFactory” class=“org.springframework.orm.hibernate5.LocalSessionFactoryBean”>
55 <property name=“dataSource” ref=“dataSource” />
56 <property name=“hibernateProperties” >
57 <props>
58 <prop key=“dialect”>${hibernate.dialect}</prop>
59 <prop key=“show_sql”>${hibernate.show.sql}</prop>
60 <prop key=“hbm2ddl.auto”>${hibernate.hbm2ddl.auto}</prop>
61 </props>
62 </property>
63
64 <property name=“packagesToScan”>
65 <list>
66 <value>com.lyyzoo.*.entity</value>
67 </list>
68 </property>
69 </bean>
70
71
72 <bean id=“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”>
73 <property name=“dataSource” ref=“dataSource”/>
74 </bean>
75 <bean id=“namedParameterJdbcTemplate” class=“org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate”>
76 <constructor-arg ref=“dataSource”/>
77 </bean>
78 <bean id=“jdbcTemplateSupport” class=“com.lyyzoo.data.jdbc.JdbcTemplateSupport”>
79 <property name=“dataSource” ref=“dataSource”/>
80 <property name=“namedParameterJdbcTemplate” ref=“namedParameterJdbcTemplate”/>
81 </bean>
82
83
84 <bean id=“transactionManager” class=“org.springframework.orm.hibernate5.HibernateTransactionManager”>
85 <property name=“sessionFactory” ref=“sessionFactory” />
86 </bean>
87
88
89 <tx:advice id=“transactionManagerAdvice” transaction-manager=“transactionManager”>
90 tx:attributes\
91 <tx:method name=“get*“ read-only=“true” />
92 <tx:method name=“load*“ read-only=“true” />
93 <tx:method name=“find*“ read-only=“true” />
94 <tx:method name=“list*“ read-only=“true” />
95 <tx:method name=“page*“ read-only=“true” />
96 <tx:method name=“query*“ read-only=“true” />
97 <tx:method name=“*“ propagation=“REQUIRED” />
98 </tx:attributes>
99 </tx:advice>
100
101
102 <aop:config proxy-target-class=“true”>
103 <aop:pointcut id=“serviceMethod” expression=“execution(* com.lyyzoo..*Service.*(..))”/>
104 <aop:advisor pointcut-ref=“serviceMethod” advice-ref=“transactionManagerAdvice”/>
105 </aop:config>
106
107
108 <context:component-scan base-package=“com.lyyzoo” />
109
110 </beans>

View Code

  然后在com.lyyzoo.data.domain包下,Page封装了分页相关的属性,如当前页数据、页码、每页大小、总页数等。com.lyyzoo.data.util包下,Sqls封装了操作sql语句的工具,比如根据传入的查询语句,返回查询总数的sql语句。在com.lyyzoo.data.jdbc包下,JdbcTemplateSupport封装了比较复杂的查询,比如根据传入的参数、分页条件等,自动查询数据、总数并封装到Page里返回。com.lyyzoo.data.dao包下,SimpleDao封装了基于Hibernate SessionFactory的简单的增删改查操作,如save(),get()等方法。dao包下,BaseDao继承SimpleDao,并代理了JdbcTemplateSupport,使得BaseDao具有SimpleDao和JdbcTemplateSupport的功能,BaseDao则作为其它dao的一个父类存在。最后,在com.lyyzoo.service包下,BaseService代理了BaseDao,进行了进一步的封装,BaseService是其它service类的父类,其它service就可以直接调用save、get、page等内置方法。

(4) lyyzoo-starter-web

  lyyzoo-starter-web自然跟web相关,在com.lyyzoo.web包下,BaseController作为controller类的父类,主要封装了返回结果集等信息。还有web相关配置,如视图解析器等内容:

1
2 <beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
3 xmlns=“http://www.springframework.org/schema/beans
4 xmlns:beans=“http://www.springframework.org/schema/beans
5 xmlns:tx=“http://www.springframework.org/schema/tx
6 xmlns:aop=“http://www.springframework.org/schema/aop
7 xmlns:context=“http://www.springframework.org/schema/context
8 xmlns:mvc=“http://www.springframework.org/schema/mvc
9 xmlns:task=“http://www.springframework.org/schema/task
10 xmlns:util=“http://www.springframework.org/schema/util
11 xmlns:jee=“http://www.springframework.org/schema/jee
12 xmlns:jdbc=“http://www.springframework.org/schema/jdbc
13 xmlns:tool=“http://www.springframework.org/schema/tool
14 xmlns:dwr=“http://www.directwebremoting.org/schema/spring-dwr
15 xsi:schemaLocation=“ 16 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 17 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 18 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd 19 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd 20 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd 21 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd 22 http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.2.xsd 23 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd 24 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd 25 http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd 26 http://www.directwebremoting.org/schema/spring-dwr http://www.directwebremoting.org/schema/spring-dwr-3.0.xsd
27 >
28
29 <description>Spring Web</description>
30
31
32 <context:property-placeholder location=“classpath*:/config/*.properties” file-encoding=“UTF-8” />
33
34
35 <context:component-scan base-package=“com.lyyzoo” use-default-filters=“false”>
36 <context:include-filter type=“annotation” expression=“org.springframework.stereotype.Controller”/>
37 <context:include-filter type=“annotation” expression=“org.springframework.web.bind.annotation.ControllerAdvice”/>
38 </context:component-scan>
39
40
41 <mvc:resources location=“/static/“ mapping=“/static/**“/>
42
43
44 <mvc:default-servlet-handler />
45
46
47 <mvc:annotation-driven />
48
49
50 <bean id=“jspViewResolver” class=“org.springframework.web.servlet.view.InternalResourceViewResolver”>
51 <property name=“prefix” value=“/WEB-INF/view”/>
52 <property name=“suffix” value=“.jsp”/>
53 </bean>
54
55 </beans>

View Code

(5) 类结构图

  类结构图参考《底层类结构.vsd》

  

五、数据库与实体设计

  在开始开发一个系统之前,首先需要做的就是根据需求分析设计系统的实体对象以及对应的数据库表结构,这是开发的基础。

  根据前面的需求分析设计的功能模块,实体对象可以分为5个模块,分别是系统模块(system)、基础模块(base)、采购模块(purchase)、销售模块(sale)、库存模块(stock)。下面是实体清单:

  

  有了清单之后,利用PowerDesigner进行数据库物理模型设计。由于拥有对数据库的完全控制权,所以不对表设置约束,所有的约束在程序代码中进行控制。下面列出各个实体的属性即对应的表,具体可参考《数据库物理模型.pdm》。物理模型设计完成后,创建名为gpss的数据库,然后创建各个表。

  数据库模型:

  

六、系统功能实现

1.创建工程

  需求分析做完了,技术没问题,底层架构也设计好了,数据库设计好了,前面的所有准备工作做完了,下面就要进行燃气管进销存系统的编码实现了。首先要做的工作就是创建工程,项目名拟为gpss,即燃气管进销存(Gas Purchase Sale Stock)的缩写,工程名则为lyyzoo-gpss。

  在IDEA中创建lyyzoo-gpss的maven工程,继承lyyzoo。同时,在lyyzoo-gpss下创建两个子模块,分别为lyyzoo-gpss-base和lyyzoo-gpss-web,base模块包含了系统的dao、entity、service等层次,主要为java文件;web则包含了controller层、jsp、js等web相关资源文件。

  lyyzoo-gpss目录结构:

  

  lyyzoo-gpss > pom.xml:

1 <project xmlns=“http://maven.apache.org/POM/4.0.0“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
2 xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"\>
3 <modelVersion>4.0.0</modelVersion>
4 <parent>
5 <groupId>com.lyyzoo</groupId>
6 <artifactId>lyyzoo</artifactId>
7 <version>1.0-SNAPSHOT</version>
8 </parent>
9 <groupId>com.lyyzoo.gpss</groupId>
10 <artifactId>lyyzoo-gpss</artifactId>
11 <name>lyyzoo :: gpss</name>
12 <packaging>pom</packaging>
13
14 <modules>
15 <module>lyyzoo-gpss-base</module>
16 <module>lyyzoo-gpss-web</module>
17 </modules>
18
19 <profiles>
20
21 <profile>
22 <id>dev</id>
23 <activation>
24 <property>
25 <name>env</name>
26 <value>dev</value>
27 </property>
28 </activation>
29 </profile>
30
31 <profile>
32 <id>online</id>
33 <activation>
34 <property>
35 <name>env</name>
36 <value>online</value>
37 </property>
38 </activation>
39 </profile>
40 </profiles>
41 <build>
42 <plugins>
43 <plugin>
44 <groupId>com.juvenxu.portable-config-maven-plugin</groupId>
45 <artifactId>portable-config-maven-plugin</artifactId>
46 <version>1.1.5</version>
47 <executions>
48 <execution>
49 <goals>
50 <goal>replace-package</goal>
51 </goals>
52 </execution>
53 </executions>
54 <configuration>
55 <portableConfig>src/main/resources/portable/config-${env}.xml</portableConfig>
56 </configuration>
57 </plugin>
58 </plugins>
59 </build>
60
61 </project>

View Code

  lyyzoo-gpss-base主要是dao层、service层、model层的集成,所以需要依赖于底层lyyzoo-starter-jpa、lyyzoo-starter-web。

  lyyzoo-gpss-base > pom.xml:

1 <project xmlns=“http://maven.apache.org/POM/4.0.0“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
2 xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"\>
3 <modelVersion>4.0.0</modelVersion>
4 <parent>
5 <groupId>com.lyyzoo.gpss</groupId>
6 <artifactId>lyyzoo-gpss</artifactId>
7 <version>1.0-SNAPSHOT</version>
8 </parent>
9 <artifactId>lyyzoo-gpss-base</artifactId>
10 <name>lyyzoo :: gpss :: base</name>
11 <packaging>jar</packaging>
12
13 <dependencies>
14 <dependency>
15 <groupId>com.lyyzoo</groupId>
16 <artifactId>lyyzoo-starter-jpa</artifactId>
17 </dependency>
18 <dependency>
19 <groupId>com.lyyzoo</groupId>
20 <artifactId>lyyzoo-starter-web</artifactId>
21 </dependency>
22 </dependencies>
23
24 </project>

View Code

  lyyzoo-gpss-web是web相关,是controller层所在。依赖于lyyzoo-gpss-base、lyyzoo-starter-base等。

  lyyzoo-gpss-web > pom.xml:

1 <project xmlns=“http://maven.apache.org/POM/4.0.0“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
2 xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4\_0\_0.xsd"\>
3 <modelVersion>4.0.0</modelVersion>
4 <parent>
5 <groupId>com.lyyzoo.gpss</groupId>
6 <artifactId>lyyzoo-gpss</artifactId>
7 <version>1.0-SNAPSHOT</version>
8 </parent>
9 <artifactId>lyyzoo-gpss-web</artifactId>
10 <name>lyyzoo :: gpss :: web</name>
11 <packaging>war</packaging>
12
13 <dependencies>
14 <dependency>
15 <groupId>com.lyyzoo.gpss</groupId>
16 <artifactId>lyyzoo-gpss-base</artifactId>
17 <version>${project.version}</version>
18 </dependency>
19 <dependency>
20 <groupId>com.lyyzoo</groupId>
21 <artifactId>lyyzoo-starter-base</artifactId>
22 </dependency>
23 <dependency>
24 <groupId>com.lyyzoo</groupId>
25 <artifactId>lyyzoo-starter-jpa</artifactId>
26 </dependency>
27 <dependency>
28 <groupId>com.lyyzoo</groupId>
29 <artifactId>lyyzoo-starter-web</artifactId>
30 </dependency>
31 <dependency>
32 <groupId>com.lyyzoo</groupId>
33 <artifactId>lyyzoo-starter-test</artifactId>
34 </dependency>
35
36
37 </dependencies>
38
39
40 <build>
41 <finalName>gpss</finalName>
42 <outputDirectory>src/main/webapp/WEB-INF/classes</outputDirectory>
43 </build>
44
45 </project>

View Code

2.系统配置

  工程建好后,首要要做的就是系统的配置工作了,如web.xml,这应该算是web项目的起点了。进入lyyzoo-gpss-web/src/main/webapp/WEB-INF/web.xml,进行web的配置,主要的一些配置有加载系统的配置文件、Spring、字符过滤器、SpringMVC等配置。

  一些基础的配置如下:

1
2 <web-app xmlns=“http://java.sun.com/xml/ns/javaee
3 xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance
4 xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee
5 http://java.sun.com/xml/ns/javaee/web-app\_3\_0.xsd
6 version=“3.0”>
7
8 <display-name>lyyzoo :: gpss</display-name>
9
10
11 <context-param>
12 <param-name>contextConfigLocation</param-name>
13 <param-value>classpath*:/spring/spring-base*.xml</param-value>
14 </context-param>
15
16
17 <listener>
18 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
19 </listener>
20
21
22 <filter>
23 <filter-name>encodingFilter</filter-name>
24 <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
25 <init-param>
26 <param-name>encoding</param-name>
27 <param-value>UTF-8</param-value>
28 </init-param>
29 <init-param>
30 <param-name>forceEncoding</param-name>
31 <param-value>true</param-value>
32 </init-param>
33 </filter>
34 <filter-mapping>
35 <filter-name>encodingFilter</filter-name>
36 <url-pattern>/*</url-pattern>
37 </filter-mapping>
38
39
40 <filter>
41 <filter-name>VisitFilter</filter-name>
42 <filter-class>com.lyyzoo.gpss.filter.VisitFilter</filter-class>
43 </filter>
44 <filter-mapping>
45 <filter-name>VisitFilter</filter-name>
46 <url-pattern>/admin/*</url-pattern>
47 </filter-mapping>
48
49
50 <servlet>
51 <servlet-name>SpringMVC</servlet-name>
52 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
53 <init-param>
54 <param-name>contextConfigLocation</param-name>
55 <param-value>classpath*:/spring/spring-web*.xml</param-value>
56 </init-param>
57 <load-on-startup>1</load-on-startup>
58 </servlet>
59 <servlet-mapping>
60 <servlet-name>SpringMVC</servlet-name>
61 <url-pattern>/</url-pattern>
62 </servlet-mapping>
63
64
65 <error-page>
66 <error-code>404</error-code>
67 <location>/error/404</location>
68 </error-page>
69 <error-page>
70 <error-code>500</error-code>
71 <location>/error/500</location>
72 </error-page>
73 <error-page>
74 <exception-type>java.lang.Throwable</exception-type>
75 <location>/error/500</location>
76 </error-page>
77
78
79 <welcome-file-list>
80 <welcome-file>/</welcome-file>
81 </welcome-file-list>
82
83 </web-app>

View Code

  接着,配置系统将会用到的一些属性,如JDBC驱动、数据库用户名和密码等。在lyyzoo-gpss-web/src/main/resources/config下,创建config.properties配置文件,这个配置文件中的属性将会被底层spring配置的文件所引用。

  比如JDBC的属性:

1 ####################################
2 # DATABASE
3 ####################################
4 # local_db
5 jdbc.driver=net.sf.log4jdbc.DriverSpy
6 jdbc.url=jdbc:log4jdbc:mysql://localhost:3306/gpss?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false
7 jdbc.username=root
8 jdbc.password=root
9
10 # 初始化连接 11 c3p0.initialPoolSize=5 12 # 连接池保留最小连接数 13 c3p0.minPoolSize=5 14 # 连接池保留最大连接数 15 c3p0.maxPoolSize=15 16 # 最大空闲时间 17 c3p0.maxIdleTime=600 18
19 #hibernate 20 hibernate.dialect=org.hibernate.dialect.MySQL5Dialect 21 hibernate.show.sql=false 22 hibernate.hbm2ddl.auto=update

View Code

  但是上面的配置只是本机的一个开发环境,如果我将项目发布到生产环境,那就至少需要重新修改数据库地址、用户名和密码。这样是比较麻烦的,所以,在lyyzoo-gpss-web/src/main/portable下创建两个xml文件,分别为开发环境和生产环境的:config-dev.xml和config-online.xml。

  config-online.xml:

1
2
3 <portable-config>
4 <config-file path=“WEB-INF/classes/config/config.properties”>
5
6
7 <replace key=“jdbc.driver”><![CDATA[com.mysql.jdbc.Driver]]></replace>
8 <replace key=“jdbc.url”><![CDATA[jdbc:mysql://192.168.1.91:30112/fff2f025c2b04?useUnicode=true&characterEncoding=utf-8]]></replace>
9 <replace key=“jdbc.username”>a6564a1169d94</replace>
10 <replace key=“jdbc.password”>d3e6d1aea5e04</replace>
11
12
13 <replace key=“hibernate.show.sql”>false</replace>
14 <replace key=“hibernate.hbm2ddl.auto”>none</replace>
15
16
17 <replace key=“app.debug”>false</replace>
18 <replace key=“app.env”>online</replace>
19 <replace key=“logback.level”>INFO</replace>
20 <replace key=“jdbc.resultsettable”>ERROR</replace>
21
22
23 </config-file>
24 </portable-config>

View Code

  然后配置一个“不同环境打包”的maven插件——“portable-config-maven-plugin”,通过该插件,就可以在打包的时候加上需要打包的环境,例如指定online环境,在打包时就会将config-online.xml中的属性替换到config.properties里,这样一来就不必我们手动去替换了。配置好之后,我们在打包时可以使用命令[mvn clean package –Denv=online]打包线上环境的war包。

  maven环境配置,在lyyzoo-gpss > pom.xml中配置两个环境:

1 <profiles>
2
3 <profile>
4 <id>dev</id>
5 <activation>
6 <property>
7 <name>env</name>
8 <value>dev</value>
9 </property>
10 </activation>
11 </profile>
12
13 <profile>
14 <id>online</id>
15 <activation>
16 <property>
17 <name>env</name>
18 <value>online</value>
19 </property>
20 </activation>
21 </profile>
22 </profiles>

View Code

  不同环境打包插件(portable-config-maven-plugin)的配置:

1 <build>
2 <plugins>
3 <plugin>
4 <groupId>com.juvenxu.portable-config-maven-plugin</groupId>
5 <artifactId>portable-config-maven-plugin</artifactId>
6 <version>1.1.5</version>
7 <executions>
8 <execution>
9 <goals>
10 <goal>replace-package</goal>
11 </goals>
12 </execution>
13 </executions>
14 <configuration>
15 <portableConfig>src/main/resources/portable/config-${env}.xml</portableConfig>
16 </configuration>
17 </plugin>
18 </plugins>
19 </build>

View Code

3.模块分层

3.1 lyyzoo-gpss-base

  工程建好后,创建包的结构。按照一般的分层方式,分为dao层、entity层、service层,同时,每层下按模块划分为system、base、purchase、sale、stock。然后在entity下根据表设计创建实体类。

  lyyzoo-gpss-base 目录结构:

  

3.2 lyyzoo-gpss-web

  然后是lyyzoo-gpss-web模块,该模块主要是controller层,以及静态资源文件、jsp文件等。com.lyyzoo.gpss.web作为controller层的包,同样,在web下按系统模块划分。

  lyyzoo-gpss-web 目录结构:

  

3.3 静态资源文件

  lyyzoo-gpss-web/src/main/webapp/static作为静态文件的根目录,static/lib目录作为三方类库的根目录,如ExtJs、jQuery、其它的插件等。static/css是系统css文件的根目录;static/img是图片的根目录;static/js是系统js文件根目录,/js下同样按模块划分。

  静态资源文件目录结构:

  

3.4 JSP文件

  jsp文件不能直接让用户访问,需要放到/WEB-INF下,与配置的spring视图解析器相对应,所有的jsp文件放到/WEB-INF/view目录下,view目录下按模块划分,index.jsp是系统的登录页面。/WEB-INF/layout/目录下,将jsp中需要引入的一些资源等做了整合,如ExtJs的文件、meta描述信息、taglib等,整合后,jsp中如果需要引入整合jsp即可,可减少很多重复的工作。

  taglib.jsp中引入了标签和设置了资源的路径:

1 <%@ page contentType=“text/html;charset=UTF-8” language=“java” %>
2
3 <%-- 引入标签 --%>
4 <%@taglib prefix=“c” uri=“http://java.sun.com/jsp/jstl/core“ %>
5 <%@taglib prefix=“fn” uri=“http://java.sun.com/jsp/jstl/functions“ %>
6 <%@taglib prefix=“fmt” uri=“http://java.sun.com/jsp/jstl/fmt“ %>
7 <%@taglib prefix=“sf” uri=“http://www.springframework.org/tags/form“ %>
8
9 <%-- 资源路径 --%>
10 <c:set var=“CTX” value=“${pageContext.request.contextPath}” />
11 <c:set var=“STATIC_CTX_URL” value=“${CTX}/static” />
12 <c:set var=“LIB” value=“${STATIC_CTX_URL}/lib” />
13 <c:set var=“JS” value=“${STATIC_CTX_URL}/js”/>
14 <c:set var=“CSS” value=“${STATIC_CTX_URL}/css”/>
15 <c:set var=“IMG” value=“${STATIC_CTX_URL}/img”/>

View Code

  meta.jsp中主要设置了一些meta描述信息和ICO图标:

1 <%@ page contentType=“text/html;charset=UTF-8” language=“java” %>
2 <%@ include file=“/WEB-INF/layout/taglib.jsp” %>
3
4 <meta charset=“utf-8”>
5 <meta http-equiv=“X-UA-Compatible” content=“IE=9,chrome=1”>
6
7 <meta name=“description” content=“”>
8 <meta name=“author” content=“”>
9 <meta name=“renderer” content=“webkit”>
10 <%-- 图标 --%>
11 <link type=“image/x-icon” href=“${IMG}/favicon.ico” rel=“icon”>

View Code

  extjs-neptune.jsp则引入了ExtJs相关的css和js文件,以及jQuery等: 

1 <%@ page contentType=“text/html;charset=UTF-8” language=“java” %>
2 <%@ include file=“/WEB-INF/layout/taglib.jsp” %>
3
4 <link rel=“stylesheet” type=“text/css” href=“${LIB}/ext/4.2.2/theme/ext-theme-neptune/ext-theme-neptune-all.css”/>
5 <link rel=“stylesheet” type=“text/css” href=“${LIB}/ext/4.2.2/icons/icon.css”/>
6
7 <script type=“text/javascript” src=“${LIB}/ext/4.2.2/ext-all.js”></script>
8 <script type=“text/javascript” src=“${LIB}/ext/4.2.2/locale/ext-lang-zh_CN.js”></script>
9 <script type=“text/javascript” src=“${LIB}/jquery/2.1.1/jquery.js”></script>
10 <script type=“text/javascript” src=“${JS}/Global.js”></script>
11
12 <script type=“text/javascript”>
13 window.CTX = “${CTX}”; 14 window.STATIC_CTX_URL = “${STATIC_CTX_URL}”; 15 window.LIB = “${LIB}”; 16 window.JS = “${JS}”; 17 window.CSS = “${CSS}”; 18 window.IMG = “${IMG}”; 19 </script>

View Code

  WEB-INF目录结构:

  

3.4 登录实现举例

  首先创建一个BaseController,BaseController继承底层的BaseController,增加了HttpSession,以及获取当前登录用户的方法,这样其它的controller继承该BaseController后,就可以非常方便的获得当前session和登录用户了。

  BaseController代码如下:

1 package com.lyyzoo.gpss.web; 2
3 import com.lyyzoo.gpss.entity.system.User; 4 import org.springframework.beans.factory.annotation.Autowired; 5
6 import javax.servlet.http.HttpSession; 7
8 /**
9 * 10 *

11 * 12 * @author bojiangzhou 13 * @date 2017-04-02 14 */
15 public class BaseController extends com.lyyzoo.web.BaseController { 16
17 @Autowired 18 protected HttpSession session; 19
20 public User getCurrentUser(){ 21 return (User) session.getAttribute(“currentUser”); 22 } 23
24 public Long getCurrentUserId(){ 25 return getCurrentUser().getId(); 26 } 27
28 public String getCurrentUserAccount(){ 29 return getCurrentUser().getAccount(); 30 } 31
32 }

View Code

  创建HomeController作为登录和主页的处理器。主要包含的方法有访问登录页面,访问登录后的主页,以及登录处理等。Controller需要加上@Controller标注该类为controller,使用@RequestMapping()支持访问rest形式的地址访问。HomeController中注入UserService,用于处理用户登录相关的业务。

  用户进入登录界面,jsp页面以<img src=” ${CTX}/vcode”>的形式请求验证码,验证码使用工具类生成,以流的形式输出,生成的验证码保存到session中。用户输入登录账号、密码和验证码登录系统。首先前台会检测输入是否为空等,传到后台,从session中取出验证码判断验证码是否正确,不正确则刷新验证码并且需要用户重新输入验证码。验证码通过后,使用登录账号和密码查找数据库,如果有,则将该用户保存到session中,跳转到管理页面,登录成功。否则提示用户名或密码错误。

  HomeController代码如下:

1 package com.lyyzoo.gpss.web; 2
3 import com.lyyzoo.gpss.entity.system.User; 4 import com.lyyzoo.gpss.service.system.UserService; 5 import com.lyyzoo.gpss.util.VCodeGenerator; 6 import com.lyyzoo.util.Cryptos; 7 import com.lyyzoo.util.Strings; 8 import org.springframework.beans.BeanUtils; 9 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.stereotype.Controller; 11 import org.springframework.web.bind.annotation.PathVariable; 12 import org.springframework.web.bind.annotation.RequestMapping; 13 import org.springframework.web.bind.annotation.RequestMethod; 14
15 import javax.imageio.ImageIO; 16 import javax.servlet.http.HttpServletResponse; 17 import javax.servlet.http.HttpSession; 18 import java.awt.image.BufferedImage; 19 import java.io.IOException; 20
21 /**
22 *


23 *
24 * @author bojiangzhou 25 * @date 2017-03-29
26 */
27 @Controller
28 @RequestMapping(“”)
29 public class HomeController extends BaseController { 30
31 @Autowired
32 private UserService userService; 33
34 /**
35 * 到首页/登录页面
36 */
37 @RequestMapping(value = {“”, “/“, “/index”, “/login”})
38 public String index(){ 39 return “/index”;
40 }
41
42 /**
43 * 管理员主页
44 */
45 @RequestMapping(“/admin/home”)
46 public String toHome(){ 47 return “/home/home”;
48 }
49
50 /**
51 * 登录
52 */
53 @RequestMapping(value = “/login”, method = RequestMethod.POST) 54 public String login(String account, String password, String vcode, HttpSession session){ 55 String redirect = REDIRECT(“/login”);
56 //基本验证
57 if(Strings.isNullOrEmpty(account)){
58 session.setAttribute(“errorMsg”, “账号必须填写!”);
59 return redirect; 60 }
61 if(Strings.isNullOrEmpty(password)){
62 session.setAttribute(“errorMsg”, “密码必须填写!”);
63 return redirect; 64 }
65 if(Strings.isNullOrEmpty(vcode)){
66 session.setAttribute(“errorMsg”, “验证码必须填写!”);
67 return redirect; 68 }
69 //验证码
70 String sessionVcode = (String) session.getAttribute(“vcode”);
71 if(!vcode.equalsIgnoreCase(sessionVcode)){
72 session.setAttribute(“errorMsg”, “验证码错误!”);
73 return redirect; 74 }
75 //验证用户名和密码
76 password = Cryptos.encryptMd5(password); 77 User loginUser = userService.login(account, password); 78 if(loginUser == null){
79 session.setAttribute(“errorMsg”, “账号或密码错误!”);
80 return redirect; 81 }
82 if(loginUser.getIsLocked() == User.IsLocked.YES){ 83 session.setAttribute(“errorMsg”, “账号已锁定,不能登录!”);
84 return redirect; 85 }
86
87 //保存到session的时候清除密码
88 User currentUser = new User(); 89 BeanUtils.copyProperties(loginUser, currentUser);
90 currentUser.setPassword(null);
91
92 //登录成功
93 session.setAttribute(“currentUser”, currentUser);
94
95 return REDIRECT(“/admin/home”);
96 }
97
98 /**
99 * 获取验证码 100 */
101 @RequestMapping(“/vcode”) 102 public void getVCode(HttpSession session, HttpServletResponse response) throws IOException { 103 //创建验证码生成器对象
104 VCodeGenerator vcGenerator = new VCodeGenerator(); 105 //生成验证码
106 String vcode = vcGenerator.generatorVCode(); 107 //将验证码保存在session域中,以便判断验证码是否正确
108 session.setAttribute(“vcode”, vcode); 109 //生成验证码图片
110 BufferedImage vImg = vcGenerator.generatorRotateVCodeImage(vcode, true); 111 //输出图像
112 ImageIO.write(vImg, “gif”, response.getOutputStream()); 113 } 114
115 /**
116 * 退出系统 117 */
118 @RequestMapping(“/logoff”) 119 public String logoff(HttpSession session){ 120 session.invalidate(); 121 return REDIRECT(“/“); 122 } 123
124 @RequestMapping(“/function”) 125 public String function(){ 126 return “/home/function”; 127 } 128
129 @RequestMapping(“/welcome”) 130 public String welcome(){ 131 return “/home/welcome”; 132 } 133
134 /**
135 * 错误页面 136 * @param code 137 * @return
138 */
139 @RequestMapping(“/error/{code}”) 140 public String error(@PathVariable String code) { 141 return “/error/“ + code; 142 } 143
144
145
146 }

View Code

  UserService中根据用户名和密码获取用户:

1 package com.lyyzoo.gpss.service.system; 2
3 @Service
4 public class UserService extends BaseService { 5 @Autowired
6 private UserDao userDao; 7 /**
8 * 获取登录用户
9 */
10 public User login(String account, String password){ 11 Map<String, Object> filter = new HashMap<>(); 12 filter.put(“account”, account); 13 filter.put(“password”, password); 14 filter.put(“state”, Applications.Flag.YES); 15
16 User loginUser = get(filter); 17 return loginUser; 18 } 19 }

View Code

七、系统的调试与部署

1.测试

  系统开发完成后,首先需要在本地整体测试,从登录开始,每个模块,每个功能,每个流程具体的去测试。

  首先测试如果未登录,用户是不能访问管理页面的,直接在地址栏输入访问地址看是否跳转到登录页面。然后至少测试各个角色相关的账号登录是否正常,登录后,每个角色拥有的菜单是否显示正常。

  其它模块的测试,使用各个角色对应的账号,登录系统,进行相应功能的测试。如管理员进入系统录入基础数据,商品信息、仓库信息等。采购管理员录入供应商信息,录入采购订单,提交审核。销售管理员录入客户信息,录入销售订单,提交审核。库存管理员审核采购订单,审核通过,则库存增加;审核销售订单,审核通过,则库存减少。查看库存信息,相应的操作之后,库存量是否正确。测试结果可查看系统测试截图。

  系统容错性测试,主要是测试输入一些错误的数据类型以及超出范围的数值测试系统在异常条件下的行为。系统在这方面做得比较好,如果用户输入了一些非法的数据,会立即提醒用户输入正确的数据。首先会在前台判断用户输入的数据的合法性、是否必须输入等,数据传到后台后,还会在代码里判断一次数据是否正确,才会保存到数据库。而系统使用的Jdbc也能在一定程度上防止SQL注入等问题。如果系统发生一些无法预测的异常,也会以友好的界面提示用户,以便技术员及时维护系统。

  总体来说,整个的测试过程比较顺利,也存在一些小问题,就立即修复了。功能全部实现了,该系统能满足一个基本的进销存流程,以后也还可以扩展。

2.部署

  我这里使用磨泊云来部署项目,磨泊云使用简单且在一定限度内免费,部署测试比较合适。首先在磨泊云上创建名为gpss的Java应用,接着创建mysql服务,并将其绑定到该java应用,复制数据库连接到配置文件中。导出本地的gpss数据库,导入到创建的mysql应用里。然后在IDEA中使用mvn clean package –Denv=online命令打包线上环境的war包,将war包发布到磨泊云上。启动项目,然后访问,测试,一些都正常。可访问域名http://gpss.butterfly.mopaasapp.com/查看,由于没处理好ext的兼容性问题,建议使用谷歌浏览器查看。后面再学学如何部署到像阿里云等云上。

八、总结

  通过自己一步步去设计完成这个燃气管进销存系统,让我对软件的架构、设计、框架、编码、以及各种工具的使用更加熟练,熟悉了软件的开发流程。这个系统更是大学四年所学知识的一个产物,从拿到论文命题开始,理解命题,查阅相关资料,进行需求分析,设计功能模块,设计数据库;然后结合自己在实习期的工作经验以及公司的架构设计,搭建了一个自己的底层。在这个底层的基础上进行系统的开发。

  经过近一个月的毕业设计,掌握了软件开发各个方面的相关知识,熟悉了整个流程,也检验了自己大学四年的知识水平。显然也还是有很多不足的,比如该系统中虽然使用SSH框架,但其框架的原理实现上还有很多不明白。个人认为学习一定要知其然也要知其所以然,尤其对于技术,需要去了解它的底层原理,才能提高自己的能力。这些就在以后的工作中去逐渐提高。

  写到这里就基本完了,大学四年的最后一篇博客,以后就是在工作中去学习了。

  很开心,要离开学校了,可以去工作找money了;很忧伤,要离开学校了,一生中可能最舒适的时光就是大学时光了。

  我很喜欢我的学校 —— 西南大学,坐落于缙云山下,环境优美、美女如云,哈哈!

九、附件

  github地址:https://github.com/bojiangzhou/lyyzoo-gpss

  论文地址:https://download.csdn.net/download/u013244234/10281005

  最后,系统开发相关文件分享出来:燃气管网设备进销存系统

  压缩包目录如下:

  

首先,Qt没有封装加解密算法库(其实有个哈希函数的函数)。介于OpenSSL函数封装不友好,以及先前爆发的心脏滴血漏洞广受诟病,我们考虑在C++上使用一种新的,并且封装友好的,OOAD程度更高的加解密算法库Crypto++。

笔者此篇文章发布前官方已更新到5.6.5版本。官网:https://www.cryptopp.com.API参考文档网址:https://www.cryptopp.com/docs/ref/

从官网下载好后,解压,由于自带了VC的项目文件,所以使用Visual Stdio部署比较容易,网上教程也很多。这里我们介绍在Qt上的部署问题。主要问题在于使用mingw编译该源码。在linux上也能很好的解决问题。

1.下载cryptopp565.zip。笔者已经成功通过mingw编译。
解压cryptopp565.zip to D:\cryptopp(或者D:\cryptopp565)

2.接下来很重要!!!使用记事本或者其他文本编辑器编辑D:\cryptopp\fipstest.cpp文件,将其中的每一个‘OutputDebugString’都替换为 ‘OutputDebugStringA’(一共三个地方替换)。记得要保存!记得要保存!记得要保存!

3.删除D:\cryptopp\GNUmakefile文件(不然之后的编译会报错)

4.打开Windows控制台,这里切换目录到crypto++库目录,输入如下命令即可
D:
cd \cryptopp
qmake -project

此时该目录下会产生cryptopp.pro文件。绝对路径为D:\cryptopp\cryptopp.pro

5.这里我们使用QtCreator打开cryptopp.pro。也可使用文本编辑器直接打开。
将第一行的
TEMPLATE = app 修改为 TEMPLATE = lib

并且在最后添加一行:LIBS += -lws2_32

6.在控制台中输入下列命令进行编译
qmake
mingw32-make all

7.等待编译结束 (通常需要几分钟)4-6步命令的截图如下图:

8.之后编译完成后,我们需要拷贝生成的两个文件

  1)D:\cryptopp\release\libcryptopp552.a 拷贝到 \lib
  2)D:\cryptopp552\release\cryptopp552.dll拷贝到\bin
这里代表你自己的Qt安装目录

9.在\include下创建一个名字为cryptopp的文件夹
从D:\cryptopp拷贝所有头文件header (.h)到 \include\cryptopp.

10.现在我们在Qt工程里测试下crypto++库

计算md5哈希值:

main.cpp

复制代码

1 #include
2
3 #define CRYPTOPP_DEFAULT_NO_DLL
4 #include <cryptopp/dll.h>
5 #ifdef CRYPTOPP_WIN32_AVAILABLE
6 #include <windows.h>
7 #endif
8 #include <cryptopp/md5.h>
9
10 USING_NAMESPACE(CryptoPP) 11 USING_NAMESPACE(std) 12 const int MAX_PHRASE_LENGTH=250; 13
14 int main(int argc, char *argv[]) { 15
16 CryptoPP::MD5 hash; 17 byte digest[ CryptoPP::MD5::DIGESTSIZE ]; 18 std::string message = “Hello World!”; 19
20 hash.CalculateDigest( digest, (const byte*)message.c_str(), message.length()); 21
22 CryptoPP::HexEncoder encoder; 23 std::string output; 24 encoder.Attach( new CryptoPP::StringSink( output ) ); 25 encoder.Put( digest, sizeof(digest) ); 26 encoder.MessageEnd(); 27
28 std::cout << “Input string: “ << message << std::endl; 29 std::cout << “MD5: “ << output << std::endl; 30
31 return 0; 32 }

复制代码

 

这一步很关键,否则会报错!! 在.pro文件里最后添加下列两行!
LIBS += -lcryptopp552
CONFIG+=console

接下来程序屏幕会显示:
Input string: Hello World!
MD5: ED076287532E86365E841E92BFC50D8C

出现这个代表测试成功,部署完成。

一、FastDFS介绍

FastDFS开源地址:https://github.com/happyfish100

参考:分布式文件系统FastDFS设计原理 

参考:FastDFS分布式文件系统

个人封装的FastDFS Java API:https://github.com/bojiangzhou/lyyzoo-fastdfs-java

1、简介

FastDFS 是一个开源的高性能分布式文件系统(DFS)。 它的主要功能包括:文件存储,文件同步和文件访问,以及高容量和负载平衡。主要解决了海量数据存储问题,特别适合以中小文件(建议范围:4KB < file_size <500MB)为载体的在线服务。

FastDFS 系统有三个角色:跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)。

Tracker Server:跟踪服务器,主要做调度工作,起到均衡的作用;负责管理所有的 storage server和 group,每个 storage 在启动后会连接 Tracker,告知自己所属 group 等信息,并保持周期性心跳。

Storage Server:存储服务器,主要提供容量和备份服务;以 group 为单位,每个 group 内可以有多台 storage server,数据互为备份。

Client:客户端,上传下载数据的服务器,也就是我们自己的项目所部署在的服务器。

 

2、FastDFS的存储策略

为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起到了冗余备份和负载均衡的作用。

在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服务。当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。

3、FastDFS的上传过程

FastDFS向使用者提供基本文件访问接口,比如upload、download、append、delete等,以客户端库的方式提供给用户使用。

Storage Server会定期的向Tracker Server发送自己的存储信息。当Tracker Server Cluster中的Tracker Server不止一个时,各个Tracker之间的关系是对等的,所以客户端上传时可以选择任意一个Tracker。

当Tracker收到客户端上传文件的请求时,会为该文件分配一个可以存储文件的group,当选定了group后就要决定给客户端分配group中的哪一个storage server。当分配好storage server后,客户端向storage发送写文件请求,storage将会为文件分配一个数据存储目录。然后为文件分配一个fileid,最后根据以上的信息生成文件名存储文件。

4、FastDFS的文件同步

写文件时,客户端将文件写至group内一个storage server即认为写文件成功,storage server写完文件后,会由后台线程将文件同步至同group内其他的storage server。

每个storage写文件后,同时会写一份binlog,binlog里不包含文件数据,只包含文件名等元信息,这份binlog用于后台同步,storage会记录向group内其他storage同步的进度,以便重启后能接上次的进度继续同步;进度以时间戳的方式进行记录,所以最好能保证集群内所有server的时钟保持同步。

storage的同步进度会作为元数据的一部分汇报到tracker上,tracke在选择读storage的时候会以同步进度作为参考。

5、FastDFS的文件下载

客户端uploadfile成功后,会拿到一个storage生成的文件名,接下来客户端根据这个文件名即可访问到该文件。

跟upload file一样,在downloadfile时客户端可以选择任意tracker server。tracker发送download请求给某个tracker,必须带上文件名信息,tracke从文件名中解析出文件的group、大小、创建时间等信息,然后为该请求选择一个storage用来服务读请求。

二、安装FastDFS环境

0、前言

操作环境:CentOS7 X64,以下操作都是单机环境。

我把所有的安装包下载到/softpackages/下,解压到当前目录。

先做一件事,修改hosts,将文件服务器的ip与域名映射(单机TrackerServer环境),因为后面很多配置里面都需要去配置服务器地址,ip变了,就只需要修改hosts即可。

# vim /etc/hosts

增加如下一行,这是我的IP
192.168.51.128 file.ljzsg.com

如果要本机访问虚拟机,在C:\Windows\System32\drivers\etc\hosts中同样增加一行

1、下载安装 libfastcommon

libfastcommon是从 FastDFS 和 FastDHT 中提取出来的公共 C 函数库,基础环境,安装即可 。

① 下载libfastcommon

# wget https://github.com/happyfish100/libfastcommon/archive/V1.0.7.tar.gz

② 解压

# tar -zxvf V1.0.7.tar.gz

cd libfastcommon-1.0.7

③ 编译、安装

# ./make.sh

./make.sh install

④ libfastcommon.so 安装到了/usr/lib64/libfastcommon.so,但是FastDFS主程序设置的lib目录是/usr/local/lib,所以需要创建软链接。

# ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so

ln -s /usr/lib64/libfastcommon.so /usr/lib/libfastcommon.so

ln -s /usr/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so

ln -s /usr/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so

2、下载安装FastDFS

① 下载FastDFS

# wget https://github.com/happyfish100/fastdfs/archive/V5.05.tar.gz

② 解压

# tar -zxvf V5.05.tar.gz

cd fastdfs-5.05

③ 编译、安装

# ./make.sh

./make.sh install

④ 默认安装方式安装后的相应文件与目录
  A、服务脚本:

/etc/init.d/fdfs_storaged
/etc/init.d/fdfs_tracker

B、配置文件(这三个是作者给的样例配置文件) :

/etc/fdfs/client.conf.sample
/etc/fdfs/storage.conf.sample
/etc/fdfs/tracker.conf.sample

C、命令工具在 /usr/bin/ 目录下:

复制代码

fdfs_appender_test
fdfs_appender_test1
fdfs_append_file
fdfs_crc32
fdfs_delete_file
fdfs_download_file
fdfs_file_info
fdfs_monitor
fdfs_storaged
fdfs_test
fdfs_test1
fdfs_trackerd
fdfs_upload_appender
fdfs_upload_file
stop.sh
restart.sh

复制代码

⑤ FastDFS 服务脚本设置的 bin 目录是 /usr/local/bin, 但实际命令安装在 /usr/bin/ 下。

两种方式:

》 一是修改FastDFS 服务脚本中相应的命令路径,也就是把 /etc/init.d/fdfs_storaged 和 /etc/init.d/fdfs_tracker 两个脚本中的 /usr/local/bin 修改成 /usr/bin。

     # vim fdfs_trackerd
使用查找替换命令进统一修改:%s+/usr/local/bin+/usr/bin

vim fdfs_storaged

使用查找替换命令进统一修改:%s+/usr/local/bin+/usr/bin

  》 二是建立 /usr/bin 到 /usr/local/bin 的软链接,我是用这种方式。  

# ln -s /usr/bin/fdfs_trackerd /usr/local/bin

ln -s /usr/bin/fdfs_storaged /usr/local/bin

ln -s /usr/bin/stop.sh /usr/local/bin

ln -s /usr/bin/restart.sh /usr/local/bin

3、配置FastDFS跟踪器(Tracker)

配置文件详细说明参考:FastDFS 配置文件详解

① 进入 /etc/fdfs,复制 FastDFS 跟踪器样例配置文件 tracker.conf.sample,并重命名为 tracker.conf。

# cd /etc/fdfs

cp tracker.conf.sample tracker.conf

vim tracker.conf

② 编辑tracker.conf ,标红的需要修改下,其它的默认即可。

复制代码

# 配置文件是否不生效,false 为生效
disabled=false

提供服务的端口

port=22122

Tracker 数据和日志目录地址(根目录必须存在,子目录会自动创建)

base_path=/ljzsg/fastdfs/tracker

HTTP 服务端口

http.server_port=80

复制代码

③ 创建tracker基础数据目录,即base_path对应的目录

# mkdir -p /ljzsg/fastdfs/tracker

④ 防火墙中打开跟踪端口(默认的22122)

复制代码

# vim /etc/sysconfig/iptables
添加如下端口行:
-A INPUT -m state –state NEW -m tcp -p tcp –dport 22122 -j ACCEPT
重启防火墙:

service iptables restart

复制代码

⑤ 启动Tracker

初次成功启动,会在 /ljzsg/fdfsdfs/tracker/ (配置的base_path)下创建 data、logs 两个目录。

可以用这种方式启动

/etc/init.d/fdfs_trackerd start

也可以用这种方式启动,前提是上面创建了软链接,后面都用这种方式

service fdfs_trackerd start

查看 FastDFS Tracker 是否已成功启动 ,22122端口正在被监听,则算是Tracker服务安装成功。

# netstat -unltp|grep fdfs

关闭Tracker命令:

# service fdfs_trackerd stop

⑥ 设置Tracker开机启动

# chkconfig fdfs_trackerd on

或者:
# vim /etc/rc.d/rc.local
加入配置:
/etc/init.d/fdfs_trackerd start

⑦ tracker server 目录及文件结构

Tracker服务启动成功后,会在base_path下创建data、logs两个目录。目录结构如下:

${base_path}
|__data
| |__storage_groups.dat:存储分组信息
| |__storage_servers.dat:存储服务器列表
|__logs
| |__trackerd.log: tracker server 日志文件

4、配置 FastDFS 存储 (Storage)

① 进入 /etc/fdfs 目录,复制 FastDFS 存储器样例配置文件 storage.conf.sample,并重命名为 storage.conf

# cd /etc/fdfs

cp storage.conf.sample storage.conf

# vim storage.conf

② 编辑storage.conf

标红的需要修改,其它的默认即可。

复制代码

# 配置文件是否不生效,false 为生效
disabled=false

指定此 storage server 所在 组(卷)

group_name=group1

storage server 服务端口

port=23000

心跳间隔时间,单位为秒 (这里是指主动向 tracker server 发送心跳)

heart_beat_interval=30

Storage 数据和日志目录地址(根目录必须存在,子目录会自动生成)

base_path=/ljzsg/fastdfs/storage

存放文件时 storage server 支持多个路径。这里配置存放文件的基路径数目,通常只配一个目录。

store_path_count=1

逐一配置 store_path_count 个路径,索引号基于 0。

如果不配置 store_path0,那它就和 base_path 对应的路径一样。

store_path0=/ljzsg/fastdfs/file

FastDFS 存储文件时,采用了两级目录。这里配置存放文件的目录个数。

如果本参数只为 N(如: 256),那么 storage server 在初次运行时,会在 store_path 下自动创建 N * N 个存放文件的子目录。

subdir_count_per_path=256

tracker_server 的列表 ,会主动连接 tracker_server

有多个 tracker server 时,每个 tracker server 写一行

tracker_server=file.ljzsg.com:22122# 允许系统同步的时间段 (默认是全天) 。一般用于避免高峰同步产生一些问题而设定。
sync_start_time=00:00
sync_end_time=23:59

# 访问端口
http.server_port=80

复制代码

③ 创建Storage基础数据目录,对应base_path目录

# mkdir -p /ljzsg/fastdfs/storage

这是配置的store_path0路径

mkdir -p /ljzsg/fastdfs/file

④ 防火墙中打开存储器端口(默认的 23000)

复制代码

# vim /etc/sysconfig/iptables

添加如下端口行:
-A INPUT -m state –state NEW -m tcp -p tcp –dport 23000 -j ACCEPT

重启防火墙:

service iptables restart

复制代码

⑤ 启动 Storage

启动Storage前确保Tracker是启动的。初次启动成功,会在 /ljzsg/fastdfs/storage 目录下创建 data、 logs 两个目录。

可以用这种方式启动

/etc/init.d/fdfs_storaged start

也可以用这种方式,后面都用这种

service fdfs_storaged start

查看 Storage 是否成功启动,23000 端口正在被监听,就算 Storage 启动成功。

# netstat -unltp|grep fdfs

关闭Storage命令:

# service fdfs_storaged stop

查看Storage和Tracker是否在通信:

/usr/bin/fdfs_monitor /etc/fdfs/storage.conf

⑥ 设置 Storage 开机启动

# chkconfig fdfs_storaged on

或者:
# vim /etc/rc.d/rc.local
加入配置:
/etc/init.d/fdfs_storaged start

⑦ Storage 目录

同 Tracker,Storage 启动成功后,在base_path 下创建了data、logs目录,记录着 Storage Server 的信息。

在 store_path0 目录下,创建了N*N个子目录:

5、文件上传测试

① 修改 Tracker 服务器中的客户端配置文件

# cd /etc/fdfs

cp client.conf.sample client.conf

vim client.conf

修改如下配置即可,其它默认。

# Client 的数据和日志目录
base_path=/ljzsg/fastdfs/client

Tracker端口

tracker_server=file.ljzsg.com:22122

② 上传测试

 在linux内部执行如下命令上传 namei.jpeg 图片

# /usr/bin/fdfs_upload_file /etc/fdfs/client.conf namei.jpeg

上传成功后返回文件ID号:group1/M00/00/00/wKgz6lnduTeAMdrcAAEoRmXZPp870.jpeg

返回的文件ID由group、存储目录、两级子目录、fileid、文件后缀名(由客户端指定,主要用于区分文件类型)拼接而成。

三、安装Nginx

上面将文件上传成功了,但我们无法下载。因此安装Nginx作为服务器以支持Http方式访问文件。同时,后面安装FastDFS的Nginx模块也需要Nginx环境。

Nginx只需要安装到StorageServer所在的服务器即可,用于访问文件。我这里由于是单机,TrackerServer和StorageServer在一台服务器上。

1、安装nginx所需环境

① gcc 安装

② PCRE pcre-devel 安装

# yum install -y pcre pcre-devel

③ zlib 安装

# yum install -y zlib zlib-devel

④ OpenSSL 安装

# yum install -y openssl openssl-devel

2、安装Nginx

① 下载nginx

# wget -c https://nginx.org/download/nginx-1.12.1.tar.gz

② 解压

# tar -zxvf nginx-1.12.1.tar.gz

cd nginx-1.12.1

③ 使用默认配置

④ 编译、安装

⑤ 启动nginx

复制代码

# cd /usr/local/nginx/sbin/

./nginx

其它命令

./nginx -s stop

./nginx -s quit

./nginx -s reload

复制代码

⑥ 设置开机启动

复制代码

# vim /etc/rc.local

添加一行:
/usr/local/nginx/sbin/nginx

# 设置执行权限
# chmod 755 rc.local

复制代码

⑦ 查看nginx的版本及模块

/usr/local/nginx/sbin/nginx -V

⑧ 防火墙中打开Nginx端口(默认的 80)

添加后就能在本机使用80端口访问了。

复制代码

# vim /etc/sysconfig/iptables

添加如下端口行:
-A INPUT -m state –state NEW -m tcp -p tcp –dport 80 -j ACCEPT

重启防火墙:

service iptables restart

复制代码

3、访问文件

简单的测试访问文件

① 修改nginx.conf

复制代码

# vim /usr/local/nginx/conf/nginx.conf

添加如下行,将 /group1/M00 映射到 /ljzsg/fastdfs/file/data
location /group1/M00 {
alias /ljzsg/fastdfs/file/data;
}

# 重启nginx
# /usr/local/nginx/sbin/nginx -s reload

复制代码

② 在浏览器访问之前上传的图片、成功。

http://file.ljzsg.com/group1/M00/00/00/wKgz6lnduTeAMdrcAAEoRmXZPp870.jpeg

四、FastDFS 配置 Nginx 模块

1、安装配置Nginx模块

① fastdfs-nginx-module 模块说明

  FastDFS 通过 Tracker 服务器,将文件放在 Storage 服务器存储, 但是同组存储服务器之间需要进行文件复制, 有同步延迟的问题。

  假设 Tracker 服务器将文件上传到了 192.168.51.128,上传成功后文件 ID已经返回给客户端。

  此时 FastDFS 存储集群机制会将这个文件同步到同组存储 192.168.51.129,在文件还没有复制完成的情况下,客户端如果用这个文件 ID 在 192.168.51.129 上取文件,就会出现文件无法访问的错误。

  而 fastdfs-nginx-module 可以重定向文件链接到源服务器取文件,避免客户端由于复制延迟导致的文件无法访问错误。

② 下载 fastdfs-nginx-module、解压

复制代码

# 这里为啥这么长一串呢,因为最新版的master与当前nginx有些版本问题。

wget https://github.com/happyfish100/fastdfs-nginx-module/archive/5e5f3566bbfa57418b5506aaefbe107a42c9fcb1.zip

解压

unzip 5e5f3566bbfa57418b5506aaefbe107a42c9fcb1.zip

重命名

mv fastdfs-nginx-module-5e5f3566bbfa57418b5506aaefbe107a42c9fcb1 fastdfs-nginx-module-master

复制代码

③ 配置Nginx

在nginx中添加模块

复制代码

# 先停掉nginx服务
# /usr/local/nginx/sbin/nginx -s stop

进入解压包目录

cd /softpackages/nginx-1.12.1/

添加模块

./configure –add-module=../fastdfs-nginx-module-master/src

重新编译、安装

make && make install

复制代码

 ④ 查看Nginx的模块

# /usr/local/nginx/sbin/nginx -V

有下面这个就说明添加模块成功

⑤ 复制 fastdfs-nginx-module 源码中的配置文件到/etc/fdfs 目录, 并修改

# cd /softpackages/fastdfs-nginx-module-master/src

# cp mod_fastdfs.conf /etc/fdfs/

修改如下配置,其它默认

复制代码

# 连接超时时间
connect_timeout=10

Tracker Server

tracker_server=file.ljzsg.com:22122

# StorageServer 默认端口
storage_server_port=23000

如果文件ID的uri中包含/group**,则要设置为true

url_have_group_name = true

Storage 配置的store_path0路径,必须和storage.conf中的一致

store_path0=/ljzsg/fastdfs/file

复制代码

⑥ 复制 FastDFS 的部分配置文件到/etc/fdfs 目录

# cd /softpackages/fastdfs-5.05/conf/

cp anti-steal.jpg http.conf mime.types /etc/fdfs/

 ⑦ 配置nginx,修改nginx.conf

# vim /usr/local/nginx/conf/nginx.conf

修改配置,其它的默认

在80端口下添加fastdfs-nginx模块

location ~/group([0-9])/M00 {
ngx_fastdfs_module;
}

注意:

listen 80 端口值是要与 /etc/fdfs/storage.conf 中的 http.server_port=80 (前面改成80了)相对应。如果改成其它端口,则需要统一,同时在防火墙中打开该端口。

location 的配置,如果有多个group则配置location ~/group([0-9])/M00 ,没有则不用配group。

⑧ 在/ljzsg/fastdfs/file 文件存储目录下创建软连接,将其链接到实际存放数据的目录,这一步可以省略。

# ln -s /ljzsg/fastdfs/file/data/ /ljzsg/fastdfs/file/data/M00

⑨ 启动nginx

# /usr/local/nginx/sbin/nginx

打印处如下就算配置成功

⑩ 在地址栏访问。

能下载文件就算安装成功。注意和第三点中直接使用nginx路由访问不同的是,这里配置 fastdfs-nginx-module 模块,可以重定向文件链接到源服务器取文件。

http://file.ljzsg.com/group1/M00/00/00/wKgz6lnduTeAMdrcAAEoRmXZPp870.jpeg

最终部署结构图(盗的图):可以按照下面的结构搭建环境。

五、Java客户端

前面文件系统平台搭建好了,现在就要写客户端代码在系统中实现上传下载,这里只是简单的测试代码。

1、首先需要搭建 FastDFS 客户端Java开发环境

① 项目中使用maven进行依赖管理,可以在pom.xml中引入如下依赖即可:

net.oschina.zcx7878 fastdfs-client-java 1.27.0.0

其它的方式,参考官方文档:https://github.com/happyfish100/fastdfs-client-java

② 引入配置文件

可直接复制包下的 fastdfs-client.properties.sample 或者 fdfs_client.conf.sample,到你的项目中,去掉.sample。

我这里直接复制 fastdfs-client.properties.sample 中的配置到项目配置文件 config.properties 中,修改tracker_servers。只需要加载这个配置文件即可

2、客户端API

个人封装的FastDFS Java API以同步到github:https://github.com/bojiangzhou/lyyzoo-fastdfs-java.git

六、权限控制

前面使用nginx支持http方式访问文件,但所有人都能直接访问这个文件服务器了,所以做一下权限控制。

FastDFS的权限控制是在服务端开启token验证,客户端根据文件名、当前unix时间戳、秘钥获取token,在地址中带上token参数即可通过http方式访问文件。

① 服务端开启token验证

复制代码

修改http.conf

vim /etc/fdfs/http.conf

设置为true表示开启token验证
http.anti_steal.check_token=true

设置token失效的时间单位为秒(s)
http.anti_steal.token_ttl=1800

密钥,跟客户端配置文件的fastdfs.http_secret_key保持一致
http.anti_steal.secret_key=FASTDFS1234567890

如果token检查失败,返回的页面
http.anti_steal.token_check_fail=/ljzsg/fastdfs/page/403.html

复制代码

记得重启服务。

② 配置客户端

客户端只需要设置如下两个参数即可,两边的密钥保持一致。

# token 防盗链功能
fastdfs.http_anti_steal_token=true

密钥

fastdfs.http_secret_key=FASTDFS1234567890

③ 客户端生成token

访问文件需要带上生成的token以及unix时间戳,所以返回的token是token和时间戳的拼接。

之后,将token拼接在地址后即可访问:file.ljzsg.com/group1/M00/00/00/wKgzgFnkaXqAIfXyAAEoRmXZPp878.jpeg?token=078d370098b03e9020b82c829c205e1f&ts=1508141521

复制代码

1 /**
2 * 获取访问服务器的token,拼接到地址后面
3 *
4 * @param filepath 文件路径 group1/M00/00/00/wKgzgFnkTPyAIAUGAAEoRmXZPp876.jpeg 5 * @param httpSecretKey 密钥 6 * @return 返回token,如: token=078d370098b03e9020b82c829c205e1f&ts=1508141521 7 */
8 public static String getToken(String filepath, String httpSecretKey){ 9 // unix seconds
10 int ts = (int) Instant.now().getEpochSecond(); 11 // token
12 String token = “null”; 13 try { 14 token = ProtoCommon.getToken(getFilename(filepath), ts, httpSecretKey); 15 } catch (UnsupportedEncodingException e) { 16 e.printStackTrace(); 17 } catch (NoSuchAlgorithmException e) { 18 e.printStackTrace(); 19 } catch (MyException e) { 20 e.printStackTrace(); 21 } 22
23 StringBuilder sb = new StringBuilder(); 24 sb.append(“token=”).append(token); 25 sb.append(“&ts=”).append(ts); 26
27 return sb.toString(); 28 }

复制代码

④ 注意事项

如果生成的token验证无法通过,请进行如下两项检查:
  A. 确认调用token生成函数(ProtoCommon.getToken),传递的文件ID中没有包含group name。传递的文件ID格式形如:M00/00/00/wKgzgFnkTPyAIAUGAAEoRmXZPp876.jpeg

  B. 确认服务器时间基本是一致的,注意服务器时间不能相差太多,不要相差到分钟级别。

⑤ 对比下发现,如果系统文件隐私性较高,可以直接通过fastdfs-client提供的API去访问即可,不用再配置Nginx走http访问。配置Nginx的主要目的是为了快速访问服务器的文件(如图片),如果还要加权限验证,则需要客户端生成token,其实已经没有多大意义。

关键是,这里我没找到FastDFS如何对部分资源加token验证,部分开放。有知道的还请留言。


OK,以上就是单机中使用FastDFS搭建文件系统并上传下载的过程。

完!!!

本篇进行Spring-data-jpa的介绍,几乎涵盖该框架的所有方面,在日常的开发当中,基本上能满足所有需求。这里不讲解JPA和Spring-data-jpa单独使用,所有的内容都是在和Spring整合的环境中实现。如果需要了解该框架的入门,百度一下,很多入门的介绍。在这篇文章的接下来一篇,会有一个系列来讲解mybatis,这个系列从mybatis的入门开始,到基本使用,和spring整合,和第三方插件整合,缓存,插件,最后会持续到mybatis的架构,源码解释,重点会介绍几个重要的设计模式,这样一个体系。基本上讲完之后,mybatis在你面前就没有了秘密,你能解决mybatis的几乎所有问题,并且在开发过程中相当的方便,驾轻就熟。

这篇文章由于介绍的类容很全,因此很长,如果你需要,那么可以耐心的看完,本人经历了很长时间的学识,使用,研究的心血浓缩成为这么短短的一篇博客。

大致整理一个提纲:

  1、Spring-data-jpa的基本介绍;

  2、和Spring整合;

  3、基本的使用方式;

  4、复杂查询,包括多表关联,分页,排序等;

现在开始:

  1、Spring-data-jpa的基本介绍:JPA诞生的缘由是为了整合第三方ORM框架,建立一种标准的方式,百度百科说是JDK为了实现ORM的天下归一,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate是一支很大的部队,使用很广泛,也很方便,能力也很强,同时Hibernate也是和JPA整合的比较良好,我们可以认为JPA是标准,事实上也是,JPA几乎都是接口,实现都是Hibernate在做,宏观上面看,在JPA的统一之下Hibernate很良好的运行。

  上面阐述了JPA和Hibernate的关系,那么Spring-data-jpa又是个什么东西呢?这地方需要稍微解释一下,我们做Java开发的都知道Spring的强大,到目前为止,企业级应用Spring几乎是无所不能,无所不在,已经是事实上的标准了,企业级应用不使用Spring的几乎没有,这样说没错吧。而Spring整合第三方框架的能力又很强,他要做的不仅仅是个最早的IOC容器这么简单一回事,现在Spring涉及的方面太广,主要是体现在和第三方工具的整合上。而在与第三方整合这方面,Spring做了持久化这一块的工作,我个人的感觉是Spring希望把持久化这块内容也拿下。于是就有了Spring-data-**这一系列包。包括,Spring-data-jpa,Spring-data-template,Spring-data-mongodb,Spring-data-redis,还有个民间产品,mybatis-spring,和前面类似,这是和mybatis整合的第三方包,这些都是干的持久化工具干的事儿。

  这里介绍Spring-data-jpa,表示与jpa的整合。

  2、我们都知道,在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在JPA中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。我们一般按照三层结构来看的话,Service层做业务逻辑处理,Dao层和数据库打交道,在Dao中,就存在着上面的对象。那么ORM框架本身提供的功能有什么呢?答案是基本的CRUD,所有的基础CRUD框架都提供,我们使用起来感觉很方便,很给力,业务逻辑层面的处理ORM是没有提供的,如果使用原生的框架,业务逻辑代码我们一般会自定义,会自己去写SQL语句,然后执行。在这个时候,Spring-data-jpa的威力就体现出来了,ORM提供的能力他都提供,ORM框架没有提供的业务逻辑功能Spring-data-jpa也提供,全方位的解决用户的需求。使用Spring-data-jpa进行开发的过程中,常用的功能,我们几乎不需要写一条sql语句,至少在我看来,企业级应用基本上可以不用写任何一条sql,当然spring-data-jpa也提供自己写sql的方式,这个就看个人怎么选择,都可以。我觉得都行。

  2.1与Spring整合我们从spring配置文件开始,为了节省篇幅,这里我只写出配置文件的结构。

复制代码

<beans xmlns=“http://www.springframework.org/schema/beans“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“ xmlns:p=“http://www.springframework.org/schema/p“ xmlns:aop=“http://www.springframework.org/schema/aop“ xmlns:tx=“http://www.springframework.org/schema/tx“ xmlns:context=“http://www.springframework.org/schema/context“ xmlns:mongo=“http://www.springframework.org/schema/data/mongo“ xmlns:jpa=“http://www.springframework.org/schema/data/jpa“ xsi:schemaLocation=“http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/data/mongo
http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"\>

<!-- 数据库连接 \-->
<context:property-placeholder location\="classpath:your-config.properties" ignore-unresolvable\="true" />
<!-- service包 \-->
<context:component-scan base-package\="your service package" />
<!-- 使用cglib进行动态代理 \-->
<aop:aspectj-autoproxy proxy-target-class\="true" />
<!-- 支持注解方式声明式事务 \-->
<tx:annotation-driven transaction-manager\="transactionManager" proxy-target-class\="true" />
<!-- dao \-->
<jpa:repositories base-package\="your dao package" repository-impl-postfix\="Impl" entity-manager-factory-ref\="entityManagerFactory" transaction-manager-ref\="transactionManager" />
<!-- 实体管理器 \-->
<bean id\="entityManagerFactory" class\="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"\>
    <property name\="dataSource" ref\="dataSource" />
    <property name\="packagesToScan" value\="your entity package" />
    <property name\="persistenceProvider"\>
        <bean class\="org.hibernate.ejb.HibernatePersistence" />
    </property\>
    <property name\="jpaVendorAdapter"\>
        <bean class\="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"\>
            <property name\="generateDdl" value\="false" />
            <property name\="database" value\="MYSQL" />
            <property name\="databasePlatform" value\="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!-- <property name="showSql" value="true" /> \-->
        </bean\>
    </property\>
    <property name\="jpaDialect"\>
        <bean class\="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
    </property\>
    <property name\="jpaPropertyMap"\>
        <map\>
            <entry key\="hibernate.query.substitutions" value\="true 1, false 0" />
            <entry key\="hibernate.default\_batch\_fetch\_size" value\="16" />
            <entry key\="hibernate.max\_fetch\_depth" value\="2" />
            <entry key\="hibernate.generate\_statistics" value\="true" />
            <entry key\="hibernate.bytecode.use\_reflection\_optimizer" value\="true" />
            <entry key\="hibernate.cache.use\_second\_level\_cache" value\="false" />
            <entry key\="hibernate.cache.use\_query\_cache" value\="false" />
        </map\>
    </property\>
</bean\>

<!-- 事务管理器 \-->
<bean id\="transactionManager" class\="org.springframework.orm.jpa.JpaTransactionManager"\>
    <property name\="entityManagerFactory" ref\="entityManagerFactory"/>
</bean\>

<!-- 数据源 \-->
<bean id\="dataSource" class\="com.alibaba.druid.pool.DruidDataSource" init-method\="init" destroy-method\="close"\>
    <property name\="driverClassName" value\="${driver}" />
    <property name\="url" value\="${url}" />
    <property name\="username" value\="${userName}" />
    <property name\="password" value\="${password}" />
    <property name\="initialSize" value\="${druid.initialSize}" />
    <property name\="maxActive" value\="${druid.maxActive}" />
    <property name\="maxIdle" value\="${druid.maxIdle}" />
    <property name\="minIdle" value\="${druid.minIdle}" />
    <property name\="maxWait" value\="${druid.maxWait}" />
    <property name\="removeAbandoned" value\="${druid.removeAbandoned}" />
    <property name\="removeAbandonedTimeout" value\="${druid.removeAbandonedTimeout}" />
    <property name\="timeBetweenEvictionRunsMillis" value\="${druid.timeBetweenEvictionRunsMillis}" />
    <property name\="minEvictableIdleTimeMillis" value\="${druid.minEvictableIdleTimeMillis}" />
    <property name\="validationQuery" value\="${druid.validationQuery}" />
    <property name\="testWhileIdle" value\="${druid.testWhileIdle}" />
    <property name\="testOnBorrow" value\="${druid.testOnBorrow}" />
    <property name\="testOnReturn" value\="${druid.testOnReturn}" />
    <property name\="poolPreparedStatements" value\="${druid.poolPreparedStatements}" />
    <property name\="maxPoolPreparedStatementPerConnectionSize" value\="${druid.maxPoolPreparedStatementPerConnectionSize}" />
    <property name\="filters" value\="${druid.filters}" />
</bean\>

<!-- 事务 \-->
<tx:advice id\="txAdvice" transaction-manager\="transactionManager"\>
    <tx:attributes\>
        <tx:method name\="\*" />
        <tx:method name\="get\*" read-only\="true" />
        <tx:method name\="find\*" read-only\="true" />
        <tx:method name\="select\*" read-only\="true" />
        <tx:method name\="delete\*" propagation\="REQUIRED" />
        <tx:method name\="update\*" propagation\="REQUIRED" />
        <tx:method name\="add\*" propagation\="REQUIRED" />
        <tx:method name\="insert\*" propagation\="REQUIRED" />
    </tx:attributes\>
</tx:advice\>
<!-- 事务入口 \-->
<aop:config\>
    <aop:pointcut id\="allServiceMethod" expression\="execution(\* your service implements package.\*.\*(..))" />
    <aop:advisor pointcut-ref\="allServiceMethod" advice-ref\="txAdvice" />
</aop:config\>

</beans>

复制代码

  2.2对上面的配置文件进行简单的解释,只对“实体管理器”和“dao”进行解释,其他的配置在任何地方都差不太多。

    1.对“实体管理器”解释:我们知道原生的jpa的配置信息是必须放在META-INF目录下面的,并且名字必须叫做persistence.xml,这个叫做persistence-unit,就叫做持久化单元,放在这下面我们感觉不方便,不好,于是Spring提供了

org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean

这样一个类,可以让你的随心所欲的起这个配置文件的名字,也可以随心所欲的修改这个文件的位置,只需要在这里指向这个位置就行。然而更加方便的做法是,直接把配置信息就写在这里更好,于是就有了这实体管理器这个bean。使用

<property name=“packagesToScan” value=“your entity package” />

这个属性来加载我们的entity。

  2.3 解释“dao”这个bean。这里衍生一下,进行一下名词解释,我们知道dao这个层叫做Data Access Object,数据库访问对象,这是一个广泛的词语,在jpa当中,我们还有一个词语叫做Repository,这里我们一般就用Repository结尾来表示这个dao,比如UserDao,这里我们使用UserRepository,当然名字无所谓,随意取,你可以意会一下我的意思,感受一下这里的含义和区别,同理,在mybatis中我们一般也不叫dao,mybatis由于使用xml映射文件(当然也提供注解,但是官方文档上面表示在有些地方,比如多表的复杂查询方面,注解还是无解,只能xml),我们一般使用mapper结尾,比如我们也不叫UserDao,而叫UserMapper。

  上面拓展了一下关于dao的解释,那么这里的这个配置信息是什么意思呢?首先base-package属性,代表你的Repository接口的位置,repository-impl-postfix属性代表接口的实现类的后缀结尾字符,比如我们的UserRepository,那么他的实现类就叫做UserRepositoryImpl,和我们平时的使用习惯完全一致,于此同时,spring-data-jpa的习惯是接口和实现类都需要放在同一个包里面(不知道有没有其他方式能分开放,这不是重点,放在一起也无所谓,影响不大),再次的,这里我们的UserRepositoryImpl这个类的定义的时候我们不需要去指定实现UserRepository接口,根据spring-data-jpa自动就能判断二者的关系。

  比如:我们的UserRepository和UserRepositoryImpl这两个类就像下面这样来写。

public interface UserRepository extends JpaRepository<User, Integer>{}
public class UserRepositoryImpl {}

  那么这里为什么要这么做呢?原因是:spring-data-jpa提供基础的CRUD工作,同时也提供业务逻辑的功能(前面说了,这是该框架的威力所在),所以我们的Repository接口要做两项工作,继承spring-data-jpa提供的基础CRUD功能的接口,比如JpaRepository接口,同时自己还需要在UserRepository这个接口中定义自己的方法,那么导致的结局就是UserRepository这个接口中有很多的方法,那么如果我们的UserRepositoryImpl实现了UserRepository接口,导致的后果就是我们势必需要重写里面的所有方法,这是Java语法的规定,如此一来,悲剧就产生了,UserRepositoryImpl里面我们有很多的@Override方法,这显然是不行的,结论就是,这里我们不用去写implements部分。

  spring-data-jpa实现了上面的能力,那他是怎么实现的呢?这里我们通过源代码的方式来呈现他的来龙去脉,这个过程中cglib发挥了杰出的作用。

  在spring-data-jpa内部,有一个类,叫做

public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>,
JpaSpecificationExecutor

我们可以看到这个类是实现了JpaRepository接口的,事实上如果我们按照上面的配置,在同一个包下面有UserRepository,但是没有UserRepositoryImpl这个类的话,在运行时期UserRepository这个接口的实现就是上面的SimpleJpaRepository这个接口。而如果有UserRepositoryImpl这个文件的话,那么UserRepository的实现类就是UserRepositoryImpl,而UserRepositoryImpl这个类又是SimpleJpaRepository的子类,如此一来就很好的解决了上面的这个不用写implements的问题。我们通过阅读这个类的源代码可以发现,里面包装了entityManager,底层的调用关系还是entityManager在进行CRUD。

  3. 下面我们通过一个完整的项目来基本使用spring-data-jpa,然后我们在介绍他的高级用法。

  a.数据库建表:user,主键自增

  

  b.对应实体:User

复制代码

@Entity
@Table(name = “user”) public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; private String password; private String birthday; // getter,setter
}

复制代码

  c.简历UserRepository接口

public interface UserRepository extends JpaRepository<User, Integer>{}

  通过上面3步,所有的工作就做完了,User的基础CRUD都能做了,简约而不简单。

  d.我们的测试类UserRepositoryTest

复制代码

public class UserRepositoryTest {

@Autowired private UserRepository userRepository;

@Test public void baseTest() throws Exception {
    User user \= new User();
    user.setName("Jay");
    user.setPassword("123456");
    user.setBirthday("2008-08-08");
    userRepository.save(user); // userRepository.delete(user); // userRepository.findOne(1);

}
}

复制代码

  测试通过。

  说到这里,和spring已经完成。接下来第三点,基本使用。

4.前面把基础的东西说清楚了,接下来就是spring-data-jpa的正餐了,真正威力的地方。

  4.1 我们的系统中一般都会有用户登录这个接口,在不使用spring-data-jpa的时候我们怎么做,首先在service层定义一个登录方法。如:

User login(String name, String password);

然后在serviceImpl中写该方法的实现,大致这样:

@Override public User login(String name, String password) { return userDao.login(name, password);
}

接下来,UserDao大概是这么个样子:

User getUserByNameAndPassword(String name, String password);

然后在UserDaoImpl中大概是这么个样子:

public User getUserByNameAndPassword(String name, String password) {
    Query query \= em.createQuery("select \* from User t where t.name = ?1 and t.password = ?2");
    query.setParameter(1, name);
    query.setParameter(2, password); return (User) query.getSingleResult();
}

ok,这个代码运行良好,那么这样子大概有十来行代码,我们感觉这个功能实现了,很不错。然而这样子真正简捷么?如果这样子就满足了,那么spring-data-jpa就没有必要存在了,前面提到spring-data-jpa能够帮助你完成业务逻辑代码的处理,那他是怎么处理的呢?这里我们根本不需要UserDaoImpl这个类,只需要在UserRepository接口中定义一个方法

User findByNameAndPassword(String name, String password);

然后在service中调用这个方法就完事了,所有的逻辑只需要这么一行代码,一个没有实现的接口方法。通过debug信息,我们看到输出的sql语句是

select * from user where name = ? and password = ?

跟上面的传统方式一模一样的结果。这简单到令人发指的程度,那么这一能力是如何实现的呢?原理是:spring-data-jpa会根据方法的名字来自动生成sql语句,我们只需要按照方法定义的规则即可,上面的方法findByNameAndPassword,spring-data-jpa规定,方法都以findBy开头,sql的where部分就是NameAndPassword,被spring-data-jpa翻译之后就编程了下面这种形态:

where name = ? and password = ?

在举个例,如果是其他的操作符呢,比如like,前端模糊查询很多都是以like的方式来查询。比如根据名字查询用户,sql就是

select * from user where name like = ?

这里spring-data-jpa规定,在属性后面接关键字,比如根据名字查询用户就成了

User findByNameLike(String name);

被翻译之后的sql就是

select * from user where name like = ?

这也是简单到令人发指,spring-data-jpa所有的语法规定如下图:

通过上面,基本CRUD和基本的业务逻辑操作都得到了解决,我们要做的工作少到仅仅需要在UserRepository接口中定义几个方法,其他所有的工作都由spring-data-jpa来完成。

 接下来:就是比较复杂的操作了,比如动态查询,分页,下面详细介绍spring-data-jpa的第二大杀手锏,强大的动态查询能力。

在上面的介绍中,对于我们传统的企业级应用的基本操作已经能够基本上全部实现,企业级应用一般都会有一个模糊查询的功能,并且是多条的查询,在有查询条件的时候我们需要在where后面接上一个 xxx = yyy 或者 xxx like ‘% + yyy + %’类似这样的sql。那么我们传统的JDBC的做法是使用很多的if语句根据传过来的查询条件来拼sql,mybatis的做法也类似,由于mybatis有强大的动态xml文件的标签,在处理这种问题的时候显得非常的好,但是二者的原理都一致,那spring-data-jpa的原理也同样很类似,这个道理也就说明了解决多表关联动态查询根儿上也就是这么回事。

  那么spring-data-jpa的做法是怎么的呢?有两种方式。可以选择其中一种,也可以结合使用,在一般的查询中使用其中一种就够了,就是第二种,但是有一类查询比较棘手,比如报表相关的,报表查询由于涉及的表很多,这些表不一定就是两两之间有关系,比如字典表,就很独立,在这种情况之下,使用拼接sql的方式要容易一些。下面分别介绍这两种方式。

  a.使用JPQL,和Hibernate的HQL很类似。

   前面说道了在UserRepository接口的同一个包下面建立一个普通类UserRepositoryImpl来表示该类的实现类,同时前面也介绍了完全不需要这个类的存在,但是如果使用JPQL的方式就必须要有这个类。如下:

复制代码

public class StudentRepositoryImpl {

@PersistenceContext private EntityManager em;
@SuppressWarnings("unchecked") public Page<Student> search(User user) {
    String dataSql \= "select t from User t where 1 = 1";
    String countSql \= "select count(t) from User t where 1 = 1"; if(null != user && !StringUtils.isEmpty(user.getName())) {
        dataSql += " and t.name = ?1";
        countSql += " and t.name = ?1";
    } Query dataQuery \= em.createQuery(dataSql);
    Query countQuery \= em.createQuery(countSql); if(null != user && !StringUtils.isEmpty(user.getName())) {
        dataQuery.setParameter(1, user.getName());
        countQuery.setParameter(1, user.getName());
    }long totalSize = (long) countQuery.getSingleResult();
    Page<User> page = new Page();
    page.setTotalSize(totalSize);
    List<User> data = dataQuery.getResultList();
    page.setData(data); return page;
}

}

复制代码

通过上面的方法,我们查询并且封装了一个User对象的分页信息。代码能够良好的运行。这种做法也是我们传统的经典做法。那么spring-data-jpa还有另外一种更好的方式,那就是所谓的类型检查的方式,上面我们的sql是字符串,没有进行类型检查,而下面的方式就使用了类型检查的方式。这个道理在mybatis中也有体现,mybatis可以使用字符串sql的方式,也可以使用接口的方式,而mybatis的官方推荐使用接口方式,因为有类型检查,会更安全。

  b.使用JPA的动态接口,下面的接口我把注释删了,为了节省篇幅,注释也没什么用,看方法名字大概都能猜到是什么意思。

复制代码

public interface JpaSpecificationExecutor { T findOne(Specification spec); List findAll(Specification spec); Page findAll(Specification spec, Pageable pageable); List findAll(Specification spec, Sort sort);
long count(Specification spec);
}

复制代码

 上面说了,使用这种方式我们压根儿就不需要UserRepositoryImpl这个类,说到这里,仿佛我们就发现了spring-data-jpa为什么把Repository和RepositoryImpl文件放在同一个包下面,因为我们的应用很可能根本就一个Impl文件都不存在,那么在那个包下面就只有一堆接口,即使把Repository和RepositoryImpl都放在同一个包下面,也不会造成这个包下面有正常情况下2倍那么多的文件,根本原因:只有接口而没有实现类。

上面我们的UserRepository类继承了JpaRepository和JpaSpecificationExecutor类,而我们的UserRepository这个对象都会注入到UserService里面,于是如果使用这种方式,我们的逻辑直接就写在service里面了,下面的代码:一个学生Student类,一个班级Clazz类,Student里面有一个对象Clazz,在数据库中是clazz_id,这是典型的多对一的关系。我们在配置好entity里面的关系之后。就可以在StudentServiceImpl类中做Student的模糊查询,典型的前端grid的模糊查询。代码是这样子的:

复制代码

@Service public class StudentServiceImpl extends BaseServiceImpl implements StudentService {

@Autowired private StudentRepository studentRepository;

@Override public Student login(Student student) { return studentRepository.findByNameAndPassword(student.getName(), student.getPassword());
}

@Override public Page<Student> search(final Student student, PageInfo page) { return studentRepository.findAll(new Specification<Student>() {
        @Override public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            
            Predicate stuNameLike \= null; if(null != student && !StringUtils.isEmpty(student.getName())) {  

           // 这里也可以root.get(“name”).as(String.class)这种方式来强转泛型类型
stuNameLike = cb.like(root. get(“name”), “%” + student.getName() + “%”);
}

            Predicate clazzNameLike \= null; if(null != student && null != student.getClazz() && !StringUtils.isEmpty(student.getClazz().getName())) {
                clazzNameLike \= cb.like(root.<String> get("clazz").<String> get("name"), "%" + student.getClazz().getName() + "%");
            } if(null != stuNameLike) query.where(stuNameLike); if(null != clazzNameLike) query.where(clazzNameLike); return null;
        }
    }, new PageRequest(page.getPage() - 1, page.getLimit(), new Sort(Direction.DESC, page.getSortName())));
}

}

复制代码

先解释下这里的意思,然后我们在结合框架的源码来深入分析。

这里我们是2个表关联查询,查询条件包括Student表和Clazz表,类似的2个以上的表方式差不多,但是正如上面所说,这种做法适合所有的表都是两两能够关联上的,涉及的表太多,或者是有一些字典表,那就使用sql拼接的方式,简单一些。

先简单解释一下代码的含义,然后结合框架源码来详细分析。两个Predicate对象,Predicate按照中文意思是判断,断言的意思,那么放在我们的sql中就是where后面的东西,比如

下面的PageRequest代表分页信息,PageRequest里面的Sort对象是排序信息。上面的代码事实上是在动态的组合最终的sql语句,这里使用了一个策略模式,或者callback,就是

studentRepository.findAll(一个接口)

studentRepository接口方法调用的参数是一个接口,而接口的实现类调用这个方法的时候,在内部,参数对象的实现类调用自己的toPredicate这个方法的实现内容,可以体会一下这里的思路,就是传一个接口,然后接口的实现自己来定义,这个思路在nettyJavaScript中体现的特别明显,特别是JavaScript的框架中大量的这种方式,JS框架很多的做法都是上来先闭包,和浏览器的命名空间分开,然后入口方法就是一个回调,比如ExtJS:

Ext.onReady(function() { // xxx
});

参数是一个function,其实在框架内部就调用了这个参数,于是这个这个方法执行了。这种模式还有一个JDK的排序集合上面也有体现,我们的netty框架也采用这种方式来实现异步IO的能力。

接下来结合框架源码来详细介绍这种机制,以及这种机制提供给我们的好处。

 这里首先从JPA的动态查询开始说起,在JPA提供的API中,动态查询大概有这么一些方法,

从名字大概可以看出这些方法的意义,跟Hibernate或者一些其他的工具也都差不多,这里我们介绍参数为CriteriaQuery类型的这个方法,如果我们熟悉多种ORM框架的话,不难发现都有一个Criteria类似的东西,中文意思是“条件”的意思,这就是各个框架构建动态查询的主体,Hibernate甚至有两种,在线和离线两种Criteria,mybatis也能从Example中创建Criteria,并且添加查询条件。

那么第一步就需要构建出这个参数CriteriaQuery类型的参数,这里使用建造者模式,

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(Student.class);

接下来:

Root root = query.from(Student.class);

在这里,我们看方法名from,意思是获取Student的Root,其实也就是个Student的包装对象,就代表这条sql语句里面的主体。接下来:

    Predicate p1 = builder.like(root.<String> get("name"), "%" + student.getName() + "%");
    Predicate p2 \= builder.equal(root.<String> get("password"), student.getPassword());

Predicate是判断的意思,放在sql语句中就是where后面 xxx = yyy, xxx like yyy这种,也就是查询条件,这里构造了2个查询条件,分别是根据student的name属性进行like查询和根据student的password进行“=”查询,在sql中就是

name like = ? and password = ?

这种形式,接下来

这样子一个完整的动态查询就构建完成了,接下来调用getSingleResult或者getResultList返回结果,这里jpa的单个查询如果为空的话会报异常,这点感觉框架设计的不好,如果查询为空直接返回一个null或者一个空的List更好一点。

这是jpa原生的动态查询方式,过程大致就是,创建builder => 创建Query => 构造条件 => 查询。这么4个步骤,这里代码运行良好,如果不使用spring-data-jpa,我们就需要这么来做,但是spring-data-jpa帮我们做得更为彻底,从上面的4个步骤中,我们发现:所有的查询除了第三步不一样,其他几步都是一模一样的,不使用spring-data-jpa的情况下,我们要么4步骤写完,要么自己写个工具类,封装一下,这里spring-data-jpa就是帮我们完成的这样一个动作,那就是在JpaSpecification这个接口中的

Page findAll(Specification spec, Pageable pageable);

这个方法,前面说了,这是个策略模式,参数spec是个接口,前面也说了框架内部对于这个接口有默认的实现类

@Repository
@Transactional(readOnly = true) public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>,
JpaSpecificationExecutor {

}

,我们的Repository接口就是继承这个接口,而通过cglib的RepositoryImpl的代理类也是这个类的子类,默认也就实现了该方法。这个方法的方法体是这样的:

复制代码

/\* \* (non-Javadoc)
 \* @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findOne(org.springframework.data.jpa.domain.Specification) \*/
public T findOne(Specification<T> spec) { try { return getQuery(spec, (Sort) null).getSingleResult();
    } catch (NoResultException e) { return null;
    }
}

复制代码

这里的

getQuery(spec, (Sort) null)

返回类型是

进入这个getQuery方法:

复制代码

/\*\* \* Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
 \* 
 \* @param spec can be {@literal null}.
 \* @param sort can be {@literal null}.
 \* @return
 \*/
protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {

    CriteriaBuilder builder \= em.getCriteriaBuilder();
    CriteriaQuery<T> query = builder.createQuery(getDomainClass());

    Root<T> root = applySpecificationToCriteria(spec, query);
    query.select(root); if (sort != null) {
        query.orderBy(toOrders(sort, root, builder));
    } return applyRepositoryMethodMetadata(em.createQuery(query));
}

复制代码

一切玄机尽收眼底,这个方法的内容和我们前面使用原生jpa的api的过程是一样的,而再进入

Root root = applySpecificationToCriteria(spec, query);

这个方法:

复制代码

/\*\* \* Applies the given {@link Specification} to the given {@link CriteriaQuery}.
 \* 
 \* @param spec can be {@literal null}.
 \* @param query must not be {@literal null}.
 \* @return
 \*/
private <S> Root<T> applySpecificationToCriteria(Specification<T> spec, CriteriaQuery<S> query) {

    Assert.notNull(query);
    Root<T> root = query.from(getDomainClass()); if (spec == null) { return root;
    }

    CriteriaBuilder builder \= em.getCriteriaBuilder();
    Predicate predicate \= spec.toPredicate(root, query, builder); if (predicate != null) {
        query.where(predicate);
    } return root;
}

复制代码

我们可以发现spec参数调用了toPredicate方法,也就是我们前面service里面匿名内部类的实现。

到这里spring-data-jpa的默认实现已经完全明了。总结一下使用动态查询:前面说的原生api需要4步,而使用spring-data-jpa只需要一步,那就是重写匿名内部类的toPredicate方法。在重复一下上面的Student和Clazz的查询代码,

复制代码

1 @Override
2 public Page search(final Student student, PageInfo page) {
4 return studentRepository.findAll(new Specification() {
5 @Override
6 public Predicate toPredicate(Root root, CriteriaQuery<?> query, CriteriaBuilder cb) { 7
8 Predicate stuNameLike = null;
9 if(null != student && !StringUtils.isEmpty(student.getName())) { 10 stuNameLike = cb.like(root. get(“name”), “%” + student.getName() + “%”); 11 } 12
13 Predicate clazzNameLike = null; 14 if(null != student && null != student.getClazz() && !StringUtils.isEmpty(student.getClazz().getName())) { 15 clazzNameLike = cb.like(root. get(“clazz”). get(“name”), “%” + student.getClazz().getName() + “%”); 16 } 17
18 if(null != stuNameLike) query.where(stuNameLike); 19 if(null != clazzNameLike) query.where(clazzNameLike); 20 return null; 21 } 22 }, new PageRequest(page.getPage() - 1, page.getLimit(), new Sort(Direction.DESC, page.getSortName()))); 23 }

复制代码

到这里位置,spring-data-jpa的介绍基本上就完成了,涵盖了该框架使用的方方面面。接下来还有一块比较实用的东西,我们看到上面第15行位置的条件查询,这里使用了一个多级的get,这个是spring-data-jpa支持的,就是嵌套对象的属性,这种做法一般我们叫方法的级联调用,就是调用的时候返回自己本身,这个在处理xml的工具中比较常见,主要是为了代码的美观作用,没什么其他的用途。

最后还有一个小问题,我们上面说了使用动态查询和JPQL两种方式都可以,在我们使用JPQL的时候,他的语法和常规的sql有点不太一样,以Student、Clazz关系为例,比如:

select * from student t left join clazz tt on t.clazz_id = tt.id

这是一个很常规的sql,但是JPQL是这么写:

select t from Student t left join t.clazz tt

left join右边直接就是t的属性,并且也没有了on t.clazz_id == tt.id,然而并不会出现笛卡尔积,这里解释一下为什么没有这个条件,在我们的实体中配置了属性的映射关系,并且ORM框架的最核心的目的就是要让我们以面向对象的方式来操作数据库,显然我们在使用这些框架的时候就不需要关心数据库了,只需要关系对象,而t.clazz_id = tt.id这个是数据库的字段,由于配置了字段映射,框架内部自己就会去处理,所以不需要on t.clazz_id = tt.id就是合理的。

  前面介绍了spring-data-jpa的使用,还有一点忘了,悲观所和乐观锁问题,这里的乐观锁比较简单,jpa有提供注解@Version,加上该注解,自动实现乐观锁,byId修改的时候sql自动变成:update … set … where id = ? and version = ?,比较方便。

 in操作的查询:

  在日常手动写sql的时候有in这种查询是比较多的,比如select * from user t where t.id in (1, 2, 3);有人说in的效率不高,要少用,但是其实只要in是主键,或者说是带有索引的,效率是很高的,mysql中如果in是子查询貌似不会走索引,不过我个人经验,在我遇到的实际应用中,in(ids)这种是比较多的,所以一般来说是没有性能问题的。

  那么,sql里面比较好写,但是如果使用spring-data-jpa的动态查询方式呢,就和前面的稍微有点区别。大致上是这么一个思路:

复制代码

if(!CollectionUtils.isEmpty(ids)) {
In in = cb.in(root. get(“id”)); for (Long id : parentIds) {
in.value(id);
}
query.where(in);
}

复制代码

  cb创建一个in的Predicate,然后给这个in赋值,最后把in加到where条件中。

手动配置锁:

  spring-data-jpa支持注解方式的sql,比如:@Query(xxx),另外,关于锁的问题,在实体中的某个字段配置@Version是乐观锁,有时候为了使用一个悲观锁,或者手动配置一个乐观锁(如果实体中没有version字段),那么可以使用@Lock这个注解,它能够被解析成为相关的锁。

一对多、多对多查询(查询条件在关联对象中时):

  1、在JPA中,一个实体中如果存在多个关联对象,那么不能同时eager获取,只能有一个是eager获取,其他只能lazy;在Hibernate当中有几种独有的解决方法,在JPA当中有2中方法,i.就是前面的改成延时加载;ii.把关联对象的List改成Set(List允许重复,在多层抓去的时候无法完成映射,Hibernate默认抓去4层,在第三层的时候如果是List就无法完成映射)。

  2、在多对多的查询中,我们可以使用JPQL,也可以使用原生SQL,同时还可以使用动态查询,这里介绍多对多的动态查询,这里有一个条件比较苛刻,那就是查询参数是关联对象的属性,一对多类似,多对一可以利用上面介绍的级联获取属性的方式。这里介绍这种方式的目的是为了更好的利用以面向对象的方式进行动态查询。

  举例:2张表,分别是Employee(id, name)和Company(id, name),二者是多对多的关系,那么当查询Employee的时候,条件是更具公司名称。那么做法如下:

复制代码

@Override public List findByCompanyName(final String companyName) {

    List<Employee> employeeList = employeeRepository.findAll(new Specification<Employee>() { public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) { // ListJoin<Employee, Company> companyJoin = root.join(root.getModel().getList("companyList", Company.class), JoinType.LEFT);
            Join<Employee, Company> companyJoin = root.join("companyList", JoinType.LEFT); return cb.equal(companyJoin.get("name"), companyName);
        }
    }); return employeeList;
}

复制代码

     我们可以使用上面注释掉的方式,也可以使用下面这种比较简单的方式。因为我个人的习惯是尽量不去写DAO的实现类,除非查询特别复杂,万不得已的情况下采用,否则我个人比较偏向于这种方式。

  上面的情况如果更为极端的话,关联多个对象,可以按照下面的方式:

复制代码

@Override public List findByCompanyName(final String companyName, final String wage) {

    List<Employee> employeeList = employeeRepository.findAll(new Specification<Employee>() { public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) { // ListJoin<Employee, Company> companyJoin = root.join(root.getModel().getList("companyList", Company.class), JoinType.LEFT);
            Join<Employee, Company> companyJoin = root.join("companySet", JoinType.LEFT);
            Join<Employee, Wage> wageJoin = root.join("wageSet", JoinType.LEFT);
            Predicate p1 \= cb.equal(companyJoin.get("name"), companyName);
            Predicate p2 \= cb.equal(wageJoin.get("name"), wage); // return cb.and(p1, p2);根据spring-data-jpa的源码,可以返回一个Predicate,框架内部会自动做query.where(p)的操作,也可以直接在这里处理,然后返回null,///              也就是下面一段源码中的实现

query.where(p1, p2); return null;
}
}); return employeeList;
}

复制代码

复制代码

/\*\* \* Applies the given {@link Specification} to the given {@link CriteriaQuery}.
 \* 
 \* @param spec can be {@literal null}.
 \* @param query must not be {@literal null}.
 \* @return
 \*/
private <S> Root<T> applySpecificationToCriteria(Specification<T> spec, CriteriaQuery<S> query) {

    Assert.notNull(query);
    Root<T> root = query.from(getDomainClass()); if (spec == null) { return root;
    }

    CriteriaBuilder builder \= em.getCriteriaBuilder();
    Predicate predicate \= spec.toPredicate(root, query, builder);

    // 这里如果我们重写的toPredicate方法的返回值predicate不为空,那么调用query.where(predicate) if (predicate != null) {
query.where(predicate);
} return root;
}

复制代码

说明:虽然说JPA中这种方式查询会存在着多次级联查询的问题,对性能有所影响,但是在一般的企业级应用当中,为了开发的便捷,这种性能牺牲一般来说是可以接受的。

  特别的:在一对多中或者多对一中,即便是fetch为eager,也会先查询主对象,再查询关联对象,但是在eager的情况下虽然是有多次查询问题,但是没有n+1问题,关联对象不会像n+1那样多查询n次,而仅仅是把关联对象一次性查询出来,因此,在企业级应用当中,访问量不大的情况下一般来说没什么问题。

  补充一段题外话,关于Hibernate/JPA/Spring-Data-Jpa与MyBatis的区别联系,这种话题很多讨论,对于Hibernate/JPA/Spring-Data-Jpa,我个人而言基本上能够熟练使用,谈不上精通,对于mybatis,由于深入阅读过几次它的源码,对mybatis的设计思想以及细化到具体的方法,属性,参数算是比较熟悉,也开发过一些mybatis的相关插件。对于这两个持久化框架,总体来说的区别是,Hibernate系列的门槛相对较高,配置比较多,相对来说难度要大一些,主要体现在各种关系的问题上,据我所知,很多人的理解其实并不深刻,很多时候甚至配置得有一定的问题,但是优势也很明显,SQL自动生成,改数据库表结构仅仅需要调整几个注解就行了,在熟练使用的基础上相对来说要便捷一点。对于mybatis来说,门槛很低,真的很低,低到分分钟就能入门的程度,我个人最喜欢也是mybatis最吸引人的地方就是灵活,特别的灵活,但是修改数据库表结构之后需要调整的地方比较多,但是利用目前比较优秀的插件,对于单表操作也基本上能够达到和Hibernate差不多的境界(会稍微牺牲一点点性能),多表的情况下就要麻烦一点。性能方面的比较,由于我没做过测试,不太好比较,不过应该mybatis要稍微高一些,毕竟他的查询SQL可控一些(当然Hibernate也支持原生sql,但是对结果集的处理不够友好)。

  之后更新:Root对象还有一批fetch方法,这个目前我很少用,后面有时间再来更新。

  补充:单表分页可以传入分页对象,比如findByName(String name, new pageRequest(0, 10));

在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等。
在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰模式来实现。

装饰(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式

装饰(Decorator)模式的主要优点有:

  • 采用装饰模式扩展对象的功能比采用继承方式更加灵活
  • 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合

其主要缺点是:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂

通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。

模式的结构

装饰模式主要包含以下角色:

  • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

装饰模式的结构图如图所示:

模式的实现#

装饰模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Program
{
static void Main(string[] args)
{
IComponent p=new ConcreteComponent();
p.Operation();
Console.WriteLine("---------------------------------");
IComponent d=new ConcreteDecoratorA(p);
d.Operation();
Console.ReadLine();
}
}


public interface IComponent
{
void Operation();
}


public class ConcreteComponent : IComponent
{
public ConcreteComponent()
{
Console.WriteLine("创建具体构件角色");
}
public void Operation()
{
Console.WriteLine("调用具体构件角色的方法Operation()");
}
}


public abstract class Decorator : IComponent
{
private IComponent component;
public Decorator(IComponent component)
{
this.component=component;
}
public virtual void Operation()
{
component.Operation();
}
}


public class ConcreteDecoratorA : Decorator
{
public ConcreteDecoratorA(IComponent component)
: base(component)
{

}
public override void Operation()
{
base.Operation();
AddedFunction();
}
public void AddedFunction()
{
Console.WriteLine("为具体构件角色增加额外的功能AddedFunction()");
}
}

程序运行结果如下:

1
2
3
4
5
创建具体构件角色
调用具体构件角色的方法Operation()
---------------------------------
调用具体构件角色的方法Operation()
为具体构件角色增加额外的功能AddedFunction()

装饰模式通常在以下几种情况使用:

  • 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
  • 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰模式却很好实现。
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

Java的java.io包和.NET的Stream类都使用了该模式。我不会就它们的实现谈论很多细节。.NET中,Stream是一个抽象类,提供了基本的行为(如Component),从抽象类Stream继承来的类FileStream,MemoryStream是ConcreteCompents,类BufferedStream,CryptoStream等是ConcreteDecorators。你能清楚地认识到它们同样忽略了Decorator

装饰模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的,如以下两种情况:

  • 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件,其结构图如图所示:

  • 如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并,其结构图如图所示:

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。
如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。
例如,你刚刚参力口工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。
在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。
所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。

中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用

中介者模式是一种对象行为型模式,其主要优点如下:

  • 降低了对象之间的耦合性,使得对象易于独立地被复用。
  • 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

其主要缺点是:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

模式的结构

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

中介者模式的结构图如图所示:

模式的实现

中介者模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class Program
{
static void Main(string[] args)
{
Mediator md=new ConcreteMediator();
Colleague c1,c2;
c1=new ConcreteColleague1();
c2=new ConcreteColleague2();
md.Register(c1);
md.Register(c2);
c1.Send();
Console.WriteLine("-------------");
c2.Send();

Console.Read();
}
}


public abstract class Mediator
{

public abstract void Register(Colleague colleague);

public abstract void Relay(Colleague colleague);
}


public class ConcreteMediator : Mediator
{
private List<Colleague> colleagues=new List<Colleague>();
public override void Register(Colleague colleague)
{
if(!colleagues.Contains(colleague))
{
colleagues.Add(colleague);
colleague.SetMediator(this);
}
}
public override void Relay(Colleague colleague)
{
foreach (var item in colleagues)
{
if (!item.Equals(colleague))
{
item.Receive();
}
}
}
}


public abstract class Colleague
{
protected Mediator mediator;
public void SetMediator(Mediator mediator)
{
this.mediator=mediator;
}
public abstract void Receive();
public abstract void Send();
}


public class ConcreteColleague1 : Colleague
{
public override void Receive()
{
Console.WriteLine("具体同事类1收到请求。");
}
public override void Send()
{
Console.WriteLine("具体同事类1发出请求。");
mediator.Relay(this);
}
}


public class ConcreteColleague2 : Colleague
{
public override void Receive()
{
Console.WriteLine("具体同事类2收到请求。");
}
public override void Send()
{
Console.WriteLine("具体同事类2发出请求。");
mediator.Relay(this);
}
}

程序的运行结果如下:

1
2
3
4
5
具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

前面分析了中介者模式的结构与特点,下面分析其以下应用场景:

  • 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

在实际开发中,通常采用以下两种方法来简化中介者模式,使开发变得更简单:

  • 不定义中介者接口,把具体中介者对象实现成为单例。
  • 同事对象不持有中介者,而是在需要的时直接获取中介者对象并调用。

下图所示是简化中介者模式的结构图:

程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Program
{
static void Main(string[] args)
{
ISimpleColleague c1,c2;
c1=new SimpleConcreteColleague1();
c2=new SimpleConcreteColleague2();
c1.Send();
Console.WriteLine("-----------------");
c2.Send();
Console.Read();
}
}


public class SimpleMediator
{
private static SimpleMediator smd=new SimpleMediator();
private List<ISimpleColleague> colleagues=new List<ISimpleColleague>();
private SimpleMediator(){}
public static SimpleMediator GetMediator()
{
return smd;
}
public void Register(ISimpleColleague colleague)
{
if(!colleagues.Contains(colleague))
{
colleagues.Add(colleague);
}
}
public void Relay(ISimpleColleague scl)
{
foreach (var item in colleagues)
{
if (!item.Equals(scl))
{
item.Receive();
}
}
}
}

public interface ISimpleColleague
{
void Receive();
void Send();
}


public class SimpleConcreteColleague1 : ISimpleColleague
{
public SimpleConcreteColleague1()
{
SimpleMediator smd=SimpleMediator.GetMediator();
smd.Register(this);
}
public void Receive()
{
Console.WriteLine("具体同事类1:收到请求。");
}
public void Send()
{
SimpleMediator smd=SimpleMediator.GetMediator();
Console.WriteLine("具体同事类1:发出请求...");
smd.Relay(this);
}
}


public class SimpleConcreteColleague2 : ISimpleColleague
{
public SimpleConcreteColleague2()
{
SimpleMediator smd=SimpleMediator.GetMediator();
smd.Register(this);
}
public void Receive()
{
Console.WriteLine("具体同事类2:收到请求。");
}
public void Send()
{
SimpleMediator smd=SimpleMediator.GetMediator();
Console.WriteLine("具体同事类2:发出请求...");
smd.Relay(this);
}
}

程序运行结果如下:

1
2
3
4
5
具体同事类1:发出请求...
具体同事类2:收到请求。
-----------------
具体同事类2:发出请求...
具体同事类1:收到请求。

在软件开发中常常遇到实现某种目标存在多种策略可供选择的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等。如果使用多重条件转移语句实现(即硬编码),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决该问题。

策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的主要优点如下:

  • 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句
  • 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码
  • 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
  • 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  • 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离

其主要缺点如下:

  • 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  • 策略模式造成很多的策略类

策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性

模式的结构#

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用

其结构图如图所示:

模式的实现#

策略模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Program
{
static void Main(string[] args)
{
Context c=new Context();
IStrategy s=new ConcreteStrategyA();
c.SetStrategy(s);
c.StrategyMethod();
Console.WriteLine("-----------------");
s=new ConcreteStrategyB();
c.SetStrategy(s);
c.StrategyMethod();
Console.Read();
}
}


public interface IStrategy
{
void StrategyMethod();
}


public class ConcreteStrategyA : IStrategy
{
public void StrategyMethod()
{
Console.WriteLine("具体策略A的策略方法被访问!");
}
}


public class ConcreteStrategyB : IStrategy
{
public void StrategyMethod()
{
Console.WriteLine("具体策略B的策略方法被访问!");
}
}


public class Context
{
private IStrategy strategy;
public IStrategy GetStrategy()
{
return strategy;
}
public void SetStrategy(IStrategy strategy)
{
this.strategy=strategy;
}
public void StrategyMethod()
{
strategy.StrategyMethod();
}
}

程序运行结果如下:

1
2
3
具体策略A的策略方法被访问!
-----------------
具体策略B的策略方法被访问!

策略模式在很多地方用到,如 Java SE 中的容器布局管理就是一个典型的实例,Java SE 中的每个容器都存在多种布局供用户选择。在程序设计中,通常在以下几种情况中使用策略模式较多:

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度,其结构图如图所示: