myBatis源码解析-数据源篇(3)
前言:我们使用mybatis时,关于数据源的配置多使用如c3p0,druid等第三方的数据源。其实mybatis内置了数据源的实现,提供了连接数据库,池的功能。在分析了缓存和日志包的源码后,接下来分析mybatis中的数据源实现。
类图:mybatis中关于数据源的源码包路径如下:
mybatis中提供了一个DataSourceFactory接口,提供了设置数据源配置信息,获取数据源方法。查看类图可知,有三个实现类分别提供了不同的数据源实现。JndiDataSourceFactory,PooledDataSourceFactory,unPooledDataSourceFactory。JndiDataSourceFactory实现较简单,此处源码略过。如下为各类的相互关系。
unPooledDataSourceFactory,PooledDataSourceFactory源码分析:unPooledDataSourceFactory实现了DataSourceFactory接口,实现了数据源配置及获取数据源方法。
// 对外提供的数据源工厂接口
public interface DataSourceFactory {
// 设置配置信息
void setProperties(Properties props);
// 获取数据源
DataSource getDataSource(); }
// 非池化的数据源工厂类
public class UnpooledDataSourceFactory implements DataSourceFactory { private static final String DRIVER_PROPERTY_PREFIX = "driver."; // 数据库驱动名前缀
private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length(); protected DataSource dataSource; // 数据源 public UnpooledDataSourceFactory() {
this.dataSource = new UnpooledDataSource(); // 构造一个非池化的数据源(下文分析数据源详细代码)
} public void setProperties(Properties properties) { // 对数据源进行配置,此处设计反射包的知识(本章重点不在这,可忽略)
Properties driverProperties = new Properties();
MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); // 将dataSource类转为metaObject类
for (Object key : properties.keySet()) {
String propertyName = (String) key;
if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { // 若是数据库驱动配置
String value = properties.getProperty(propertyName);
driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); // driverProperties存储数据库驱动参数
} else if (metaDataSource.hasSetter(propertyName)) { // 如果有set方法
String value = (String) properties.get(propertyName);
// 根据属性类型进行类型的转换,主要是 Integer, Long, Boolean 三种类型的转换
Object convertedValue = convertValue(metaDataSource, propertyName, value);
// 设置DataSource 的相关属性值
metaDataSource.setValue(propertyName, convertedValue);
} else {
throw new DataSourceException("Unknown DataSource property: " + propertyName);
}
}
// 设置 DataSource.driverProerties 属性值
if (driverProperties.size() > 0) {
metaDataSource.setValue("driverProperties", driverProperties);
}
}
// 获取数据源
public DataSource getDataSource() {
return dataSource;
}
// 对Integer, Long, Boolean 三种类型的转换
private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
Object convertedValue = value;
Class<?> targetType = metaDataSource.getSetterType(propertyName);
if (targetType == Integer.class || targetType == int.class) {
convertedValue = Integer.valueOf(value);
} else if (targetType == Long.class || targetType == long.class) {
convertedValue = Long.valueOf(value);
} else if (targetType == Boolean.class || targetType == boolean.class) {
convertedValue = Boolean.valueOf(value);
}
return convertedValue;
} }
public class PooledDataSourceFactory extends UnpooledDataSourceFactory { public PooledDataSourceFactory() {
// dataSource实现类变为PooledDataSource
this.dataSource = new PooledDataSource();
} }
unPooledDataSourceFactory主要工作是对数据源进行参数配置,并提供获取数据源方法。分析PooledDataSourceFactory源码,只是继承unPooledDataSourceFactory,将DataSource实现类改变为PooledDataSource。
unPooledDataSource源码分析:基本的数据源实现都实现了DataSource接口,重写获取数据库连接的方法。unPooledDataSource从类名可知,不支持数据库连接的池化。也就是说,每来一个获取连接请求,就新建一个数据库连接。让我们看源码验证下。
public class UnpooledDataSource implements DataSource { private ClassLoader driverClassLoader; // 数据库驱动类加载器
private Properties driverProperties; // 有关数据库驱动的参数
private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); // 缓存已注册过的数据库驱动 private String driver; // 数据库驱动
private String url; // 数据库名
private String username; // 连接用户名
private String password; // 密码 private Boolean autoCommit; // 是否自动提交
private Integer defaultTransactionIsolationLevel; // 事物隔离级别 static { // 初始化
Enumeration<Driver> drivers = DriverManager.getDrivers(); // DriverManager中已存在的数据库驱动加载到数据库驱动缓存
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
registeredDrivers.put(driver.getClass().getName(), driver);
}
}
..... public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
} // 获取数据库连接
private Connection doGetConnection(Properties properties) throws SQLException {
initializeDriver(); // 初始化数据库驱动
Connection connection = DriverManager.getConnection(url, properties); // 此处每次获取连接,就新建一个数据库连接
configureConnection(connection); // 设置数据库是否自动提交,设置数据库事物隔离级别
return connection;
} private synchronized void initializeDriver() throws SQLException {
// 若此驱动还没初始化,则进行初始化
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
if (driverClassLoader != null) {
driverType = Class.forName(driver, true, driverClassLoader);
} else {
driverType = Resources.classForName(driver);
}
// DriverManager requires the driver to be loaded via the system ClassLoader.
// http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
Driver driverInstance = (Driver)driverType.newInstance();
DriverManager.registerDriver(new DriverProxy(driverInstance));
registeredDrivers.put(driver, driverInstance);
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
} private void configureConnection(Connection conn) throws SQLException {
if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
conn.setAutoCommit(autoCommit);
}
if (defaultTransactionIsolationLevel != null) {
conn.setTransactionIsolation(defaultTransactionIsolationLevel);
}
}
....
}
以上代码是UnPooledDataSource的源码分析,可见,UnPooledDataSource并没有采用池化的方法对数据库连接进行管理。每次获取连接,就新建一个数据库连接。我们知道数据库连接的建立是个非常耗时耗资源的过程,为了统一管理这些数据库连接,mybatis为我们引入了PooledDataSource类。
PooledDataSource源码分析:PooledDataSource是数据源的重点,源码比较复杂。PooledDataSource内部使用UnPooledDataSource类创建新的数据库连接。PooledDataSource并不直接管理java.sql.connection连接,而是管理java.sql.connection的一个代理类PooledConnection。除了管理数据库连接的建立,PooledDataSource内部还使用PoolState来管理数据源的状态(即空闲连接数,活跃连接数等)。综上,总结如下,PooledDataSource使用UnPooledDataSource类为数据源创建真实的数据库连接,使用PooledConnection为数据源管理数据库连接,使用PoolState来为数据源管理数据源当前状态。
PoolConnection是一个connection代理类,里面封装了真实的连接与代理连接,现在我们先来分析PoolConnection的源码。
class PooledConnection implements InvocationHandler { // 连接代理类 private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class }; private int hashCode = 0;
private PooledDataSource dataSource; // 数据源
private Connection realConnection; // 被代理的真实连接
private Connection proxyConnection; // 代理连接
private long checkoutTimestamp; // 从连接池中取出连接的时间
private long createdTimestamp; // 连接建立的时间
private long lastUsedTimestamp; // 连接上次使用的时间
private int connectionTypeCode; // 用于标注该连接所在的连接池
private boolean valid; // 连接有效的标志
PooledConnection实现了InvocationHandler接口,则可见是一个代理对象。查看属性可知,内部有真实连接与代理连接,并附带连接的一些记录信息。查看该类的构造方法。
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true; // 该链接是否有效
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); // 使用动态代理生成连接的代理类
} /*
* Invalidates the connection
*/
// 将该链接置为无效
public void invalidate() {
valid = false;
}
查看构造方法可知,内部除了初始化一些属性外,还将连接的代理类也进行初始化了。那代理类究竟做了什么,查看重写的invoke方法源码。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 代理方法
String methodName = method.getName(); // 获取方法名
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 若是close方法,则将该连接放入数据源中
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) { // 若要执行的方法不是object方法,则检查连接的有效性
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();
}
return method.invoke(realConnection, args); //执行真实的方法
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
} private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
由源码可知,代理连接在执行方法时,会先检查此连接的有效性,然后执行真实的方法。分析完PoolConnection后,对PoolState进行源码解析。
public class PoolState { // 连接池状态信息 protected PooledDataSource dataSource; // 此状态信息关联的数据源 protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>(); // 空闲连接列表
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>(); // 活跃连接列表
protected long requestCount = 0; // 请求数
protected long accumulatedRequestTime = 0; // 累加请求所用时间
protected long accumulatedCheckoutTime = 0; // 累加占用连接所用时间
protected long claimedOverdueConnectionCount = 0; // 连接超时的数量
protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 累加超时的连接超时的时间
protected long accumulatedWaitTime = 0; // 累加等待获取连接所用时间
protected long hadToWaitCount = 0; // 等待获取连接的线程数
protected long badConnectionCount = 0; // 失效的连接数
PoolState是对DataSource的状态管理类,主要包括如累计连接超时时间,失效连接的获取等一些状态信息的管理。除了包括一些数据库连接的记录信息外,内部还维护了两个数据库连接的列表idleConnections,activeConnections.。分别用来存放空闲的数据库连接列表,活跃的数据库连接列表,针对此两个列表的操作,下文在分析PooledDataSource时会进行详细介绍。
对PoolConnection和PoolState分析结束后,具体分析PoolDataSource源码。
public class PooledDataSource implements DataSource { private static final Log log = LogFactory.getLog(PooledDataSource.class); private final PoolState state = new PoolState(this); // 维护数据源的状态 private final UnpooledDataSource dataSource; // 使用UnpooledDataSource来建立真正的连接 // OPTIONAL CONFIGURATION FIELDS
protected int poolMaximumActiveConnections = 10; // 最大活跃的连接数
protected int poolMaximumIdleConnections = 5; // 最大空闲的连接数
protected int poolMaximumCheckoutTime = 20000; // 最大checkout时间(checkOutTime指的是从数据源中获取连接到归还连接的时间)
protected int poolTimeToWait = 20000; // 最大等待时间
protected String poolPingQuery = "NO PING QUERY SET"; // 使用该语句来验证该连接是否有效
protected boolean poolPingEnabled = false;
protected int poolPingConnectionsNotUsedFor = 0; private int expectedConnectionTypeCode; // hashcode
查看PoolDataSource基本属性,可知内部使用PoolState来维护数据源的状态信息,使用UnpooledDataSource来产真正的连接。并提供了一些如设置最大空闲,活跃连接数的配置信息。作为DataSource的实现,PooledDataSource不仅提供了如popConnection获取数据库连接的接口。还提供了forceCloseAll来关闭所有数据连接。pushConnection将使用结束的数据库连接放入数据源中。现在开始分析第一个方法popConnection,流程图如下,代码中都有详细注释,请耐看。
// 获取连接
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis(); // 开始取连接时间
int localBadConnectionCount = 0; // 坏的连接数
// 连接未取到,一直循环
while (conn == null) {
synchronized (state) { // 加锁
if (state.idleConnections.size() > 0) { // 连接池中是否有空闲连接
// Pool has available connection
conn = state.idleConnections.remove(0); // 从空闲连接列表中取一个空闲连接
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else { // 连接池无空闲连接
// Pool does not have available connection
if (state.activeConnections.size() < poolMaximumActiveConnections) { // 当前活跃连接数小于连接池的最大活跃连接数
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this); // 则使用unPooledDataSource新建一个连接,并封装成代理连接PooledConnection
@SuppressWarnings("unused")
//used in logging, if enabled
Connection realConn = conn.getRealConnection(); // 获取真正的连接
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else { // 否则当前活跃连接数大于等于连接池的最大活跃连接数
// Cannot create new connection
PooledConnection oldestActiveConnection = state.activeConnections.get(0); // 从活跃连接列表中取第一个活跃连接
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); // 获取连接已经获取了多长时间
if (longestCheckoutTime > poolMaximumCheckoutTime) { // 检测该连接是否超时
// Can claim overdue connection 超时则进行统计
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection); // 将此超时连接从活跃连接列表中移除
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
// 超时且关闭了自动提交,则进行回滚
oldestActiveConnection.getRealConnection().rollback();
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); // 新建一个代理连接
oldestActiveConnection.invalidate(); // 将超时的连接设置为失效
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else { // 若没有一个连接超时,则必须等待
// Must wait
try {
if (!countedWait) {
state.hadToWaitCount++; // 等待数加一
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait); // 线程等待
state.accumulatedWaitTime += System.currentTimeMillis() - wt; // 计算累积等待时间
} catch (InterruptedException e) { // 线程阻塞中断
break;
}
}
}
}
if (conn != null) {
if (conn.isValid()) { // 若连接有效
if (!conn.getRealConnection().getAutoCommit()) {
// 回滚操作
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis()); // 设置取用连接的时间戳
conn.setLastUsedTimestamp(System.currentTimeMillis()); // 设置最后一次使用连接的时间戳
state.activeConnections.add(conn); // 添加到活跃连接列表
state.requestCount++; // 请求加一
state.accumulatedRequestTime += System.currentTimeMillis() - t; // 累计建立连接所用时间
} else { // 若此连接失效
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++; // 记录坏的连接数+1
localBadConnectionCount++;
conn = null; // 置为空,开始下一次循环
if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
} }
// 返回连接
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
} return conn;
}
经分析,获取连接的过程为,先去查找空闲连接列表,若存在空闲列表,则直接从空闲列表中拿出数据库连接。若无空闲连接,则判断当前存活的数据库连接是否超过了指定的活跃连接数,若没有超过,则新建数据库连接。若超过了,则去拿活跃连接数的第一个连接判断是否连接超时(为什么拿第一个?因为是队列,队尾插入,对头获取,对头的连接没有超时,则后面的肯定没有超时)若发现第一个连接已经超过指定的数据库连接时间,则将此连接从活跃列表中移除,并标志为失效,然后自己新建一个数据库连接。若第一个连接没有过期,则代表现在数据源不能提供任何连接了,必须等待,直接wait,释放锁,等待线程唤醒。拿到了数据库连接后,需要检查该连接是否有效,若有效,则放入活跃连接列表中,并返回给用户。
当一个连接使用完毕后,需要放回到数据源中进行管理,现在分析pushConnection源码。流程图和源码分析如下:
protected void pushConnection(PooledConnection conn) throws SQLException { synchronized (state) {
state.activeConnections.remove(conn); // 将此连接从活跃连接列表中移除
if (conn.isValid()) { // 连接有效
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { // 没有超过最大空闲连接数且是同一个连接池
state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 累加连接时间
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); // 新建一个代理连接
state.idleConnections.add(newConn); // 添加到空闲连接列表中
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
conn.invalidate(); // 将老的代理连接置为不可用
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
state.notifyAll(); // 唤醒阻塞的连接
} else { // 已达到最大空闲连接
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.getRealConnection().close(); // 直接关闭
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
conn.invalidate(); // 将代理连接置为不可用
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
连接使用结束后并不是立马释放,而是检查当前空闲列表的连接数是否已超过指定空闲的连接数,若没有超过,则放入到空闲连接列表中。否则将该连接设为无效。并唤醒阻塞中的获取连接的线程。
当用户指定变更数据源配置信息时,如数据库地址,用户名,密码等,都需要对数据源进行重置,清空现存的数据库连接后修改配置信息。现查看清空数据源的方法forceCloseAll源码。此方法较简单,就不贴流程图了。
//关闭池中所有的活跃连接和空闲连接
public void forceCloseAll() {
synchronized (state) {
expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
for (int i = state.activeConnections.size(); i > 0; i--) { // 获取所有的活跃连接
try {
PooledConnection conn = state.activeConnections.remove(i - 1); // 移除
conn.invalidate(); // 失效 Connection realConn = conn.getRealConnection();
if (!realConn.getAutoCommit()) { // 事务回滚
realConn.rollback();
}
realConn.close();
} catch (Exception e) {
// ignore
}
}
for (int i = state.idleConnections.size(); i > 0; i--) { // 获取所有的空闲连接
try {
PooledConnection conn = state.idleConnections.remove(i - 1); // 移除
conn.invalidate(); // 失效 Connection realConn = conn.getRealConnection();
if (!realConn.getAutoCommit()) { // 事务回滚
realConn.rollback();
}
realConn.close();
} catch (Exception e) {
// ignore
}
}
}
if (log.isDebugEnabled()) {
log.debug("PooledDataSource forcefully closed/removed all connections.");
}
}
经分析,forceCloseAll对所有的空闲列表中,活跃列表中的数据库连接全部移除并置为不可用。池中恢复到初始化状态。
总结:本文对mybatis中的数据源部分进行了源码解析。在学习源码的过程中,加深了对很多设计模式的理解,体会到了大神们的编程习惯,不仅仅是源码本身,更多的是思想上的理解。在学习中也知道了不急于求成,一个一个的包去分析,然后再去整和业务流程。如你对此源码也感兴趣,可以评论下,我会把自己的mybatis中文注释源码包分享。但此注释都是自己手写,不能确保准确性,仅提供参考。任重而道远,源码之路希望自己能坚持下来。
myBatis源码解析-数据源篇(3)的更多相关文章
- myBatis源码解析-反射篇(4)
前沿 前文分析了mybatis的日志包,缓存包,数据源包.源码实在有点难顶,在分析反射包时,花费了较多时间.废话不多说,开始源码之路. 反射包feflection在mybatis路径如下: 源码解析 ...
- myBatis源码解析-类型转换篇(5)
前言 开始分析Type包前,说明下使用场景.数据构建语句使用PreparedStatement,需要输入的是jdbc类型,但我们一般写的是java类型.同理,数据库结果集返回的是jdbc类型,而我们需 ...
- myBatis源码解析-日志篇(1)
上半年在进行知识储备,下半年争取写一点好的博客来记录自己源码之路.在学习源码的路上也掌握了一些设计模式,可所谓一举两得.本次打算写Mybatis的源码解读. 准备工作 1. 下载mybatis源码 下 ...
- myBatis源码解析-缓存篇(2)
上一章分析了mybatis的源码的日志模块,像我们经常说的mybatis一级缓存,二级缓存,缓存究竟在底层是怎样实现的.此次开始分析缓存模块 1. 源码位置,mybatis源码包位于org.apach ...
- 【MyBatis源码解析】MyBatis一二级缓存
MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...
- Mybatis源码解析-DynamicSqlSource和RawSqlSource的区别
XMLLanguageDriver是ibatis的默认解析sql节点帮助类,其中的方法其会调用生成DynamicSqlSource和RawSqlSource这两个帮助类,本文将对此作下简单的简析 应用 ...
- mybatis源码-解析配置文件(四-1)之配置文件Mapper解析(cache)
目录 1. 简介 2. 解析 3 StrictMap 3.1 区别HashMap:键必须为String 3.2 区别HashMap:多了成员变量 name 3.3 区别HashMap:key 的处理多 ...
- mybatis源码-解析配置文件(三)之配置文件Configuration解析
目录 1. 简介 1.1 系列内容 1.2 适合对象 1.3 本文内容 2. 配置文件 2.1 mysql.properties 2.2 mybatis-config.xml 3. Configura ...
- Mybatis源码解析,一步一步从浅入深(一):创建准备工程
Spring SpringMVC Mybatis(简称ssm)是一个很流行的java web框架,而Mybatis作为ORM 持久层框架,因其灵活简单,深受青睐.而且现在的招聘职位中都要求应试者熟悉M ...
随机推荐
- Mysql and ORM
本节内容 数据库介绍 mysql 数据库安装使用 mysql管理 mysql 数据类型 常用mysql命令 创建数据库 外键 增删改查表 权限 事务 索引 python 操作mysql ORM sql ...
- Python 爬取 42 年高考数据,告诉你高考为什么这么难?
作者 | 徐麟 历年录取率 可能很多经历过高考的人都不知道高考的全称,高考实际上是普通高等学校招生全国统一考试的简称.从1977年国家恢复高考制度至今,高考经历了许多的改革,其中最为显著的变化就是录取 ...
- 为什么SpringBoot项目里引入其他依赖不要写版本号
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> < ...
- 浏览器如何执行JS
作为JS系列的第一篇,内容当然是浏览器如何执行一段JS啦. 首先通过浏览器篇我们可以得知,JS是在渲染进程里的JS引擎线程执行的.在此之后还要了解几个概念,编译器(Compiler).解释器(Inte ...
- linux : 新服务器部署项目要做的事
环境:阿里云服务器两台,一台web,一台db,系统centos7. 用户用外网访问web server ,web server 再去访问db server. 1 阿里云控制台进入系统2 SSH进入系统 ...
- JAVA 实现将多目录多层级文件打成ZIP包后保留层级目录下载 ZIP压缩 下载
将文件夹保留目录打包为 ZIP 压缩包并下载 上周做了一个需求,要求将数据库保存的 html 界面取出后将服务器下的css和js文件一起打包压缩为ZIP文件,返回给前台:在数据库中保存的是html标签 ...
- C++语法小记---经典问题之一(malloc和new的纠缠)
malloc和new以及free和delete的区分 new和malloc以及delete和free的区别 new和delete是C++的关键字,malloc和free是库函数 new和delete会 ...
- 根据 Promise/A+ 和 ES6 规范,实现 Promise(附详细测试)
Promise 源码 https://github.com/lfp1024/promise promise-a-plus const PENDING = 'PENDING' const REJECTE ...
- django-celery 版本 常用命令
http://celery.github.io/django-celery/introduction.html #先启动服务器 python manage.py runserver #再启动worke ...
- React native项目后期调整UI总结
字体 fontSize: 14, marginLeft: 10, marginTop: 2, fontFamily: 'ABBvoiceCNSG-Regular', 布局 paddingHorizon ...