第6章 Spring JDBC支持

Spring官方:

位于Spring Framework Project下。

文档:

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#jdbc

MySQL通常更广泛地用于Web应用程序开发,特别是Linux平台上。PostgreSQL对Oracle开发人员更友好,因为它的过程语言PLpgSQL非常接近Oracle的PL/SQL语言。

6.1 介绍Lambda表达式

大多数使用了模板或回调的Spring API都可以使用lambda表达式,不限于JDBC。

6.3 研究JDBC基础结构

JDBC为Java应用程序访问存储在数据库中的数据提供了一种标准方式。JDBC基础结构的核心是针对每个数据库的驱动程序,即允许Java代码访问数据库的驱动程序。

一旦加载驱动程序,就会注册java.sql.DriverManager类。该类管理驱动程序列表并提供建立与数据库连接的静态方法。DriverManager.getConnection()方法返回驱动程序实现的java.sql.Connection接口。该接口允许针对数据库运行SQL语句。

连接(Connection)是一种稀缺资源,建立起来非常昂贵。

演示怎么用JDBC写DAO代码。

6.4 Spring JDBC基础结构

org.springframework:spring-jdbc 提供对JDBC的支持,分为5个部分:

6.5 数据库连接和数据源

javax.sql.DataSource 用来帮助管理数据库连接。DataSourceConnection之间的区别在于DataSource可以提供并管理Connection。

org.springframework.jdbc.datasource.DriverManagerDataSourceDataSource的最简单实现,通过调用DriverManager来获得连接,不支持数据库连接池。

6.6 嵌入数据库支持

Spring提供了嵌入式数据库支持,该支持会自动启动嵌入式数据库并将其作为应用程序的DataSource公开。

Spring支持HSQL(默认)、H2DERBY

以H2为例:

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
@Configuration
public class EmbeddedJdbcConfig {
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2).addScripts("classpath:db/h2/schema.sql", "classpath:db/h2/test_data.sql").build();
}
}

这里要注意脚本的顺序,DDL文件应该第一个显示,之后是DML文件。

# schema.sql
CREATE TABLE SINGER
(
ID INT NOT NULL AUTO_INCREMENT,
FIRST_NAME VARCHAR(60) NOT NULL,
LAST_NAME VARCHAR(40) NOT NULL,
BIRTH_DATE DATE,
UNIQUE UQ_SINGER_1 (FIRST_NAME, LAST_NAME),
PRIMARY KEY (ID)
); CREATE TABLE ALBUM
(
ID INT NOT NULL AUTO_INCREMENT,
SINGER_ID INT NOT NULL,
TITLE VARCHAR(100) NOT NULL,
RELEASE_DATE DATE,
UNIQUE UQ_SINGER_ALBUM_1 (SINGER_ID, TITLE),
PRIMARY KEY (ID),
CONSTRAINT FK_ALBUM FOREIGN KEY (SINGER_ID) REFERENCES SINGER (ID)
);
-- test_data.sql
INSERT INTO `singer`(`id`, `first_name`, `last_name`, `birth_date`) VALUES (1, 'John', 'Mayer', '1997-10-16');
INSERT INTO `singer`(`id`, `first_name`, `last_name`, `birth_date`) VALUES (2, 'Eric', 'Clapton', '1945-03-30');
INSERT INTO `singer`(`id`, `first_name`, `last_name`, `birth_date`) VALUES (3, 'John', 'Butler', '1975-04-01'); INSERT INTO `album`(`id`, `singer_id`, `title`, `release_date`) VALUES (1, 1, 'The Search For Everything', '2017-01-20');
INSERT INTO `album`(`id`, `singer_id`, `title`, `release_date`) VALUES (2, 1, 'Battle Studies', '2009-11-17');
INSERT INTO `album`(`id`, `singer_id`, `title`, `release_date`) VALUES (3, 2, 'From the Cradle', '1994-09-13');
public class DbConfigTest {
@Test
public void test3() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(EmbeddedJdbcConfig.class);
ctx.refresh(); DataSource dataSource = ctx.getBean("dataSource", DataSource.class);
testDataSource(dataSource);
ctx.close();
} private void testDataSource(DataSource dataSource) {
Connection connection = null;
try {
connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("select 1");
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
int mockVal = resultSet.getInt("1");
System.out.println("mockVal = " + mockVal);
}
statement.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}

对于本地开发或单元测试来说,嵌入式数据库支持是非常有用的。

6.7 在DAO类中使用DataSource

数据访问对象(DAO)模式用于将低级数据访问API或操作与高级业务服务相分离。数据访问对象模式需要以下组件:

  • DAO接口:该接口定义了在模型对象(或多个对象)上执行的标准操作;
  • DAO实现:该类提供了DAO接口的具体实现。通常使用JDBC连接或数据源来处理模型对象;
  • 模型对象也称为数据对象或实体:这是映射到数据表记录的简单POJO;

6.8 异常处理

Spring提倡使用运行时异常(非检查型异常)而不是检查型异常,Spring的SQL异常更精细。

org.springframework.jdbc.support.SQLExceptionTranslator 接口负责将通用SQL错误代码转换为Spring JDBC异常。需要配合org.springframework.jdbc.core.JdbcTemplate使用。

6.9 JdbcTemplate类

该类代表Spring JDBC支持的核心。它可以执行所有类型的SQL语句,包括DDL和DML。

JdbcTemplate类允许向数据库发出任何类型的SQL语句并返回任何类型的结果。

6.9.1 在DAO类中初始化JdbcTemplate

JdbcTemplate是线程安全的。这意味着可以选择在Spring的配置中初始化一个JdbcTemplate实例,并将其注入到所有的DAO bean中。

// 定义jdbcTemplate和DAO
@Bean
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource());
return jdbcTemplate;
} @Bean
public SingerDao singerDao() {
JdbcSingerDao dao = new JdbcSingerDao();
dao.setJdbcTemplate(jdbcTemplate());
return dao;
} // 使用jdbcTemplate
@Override
public String findNameById(Long id) {
String name = jdbcTemplate.queryForObject("select first_name || ' ' || last_name from singer where id = ?", new Object[]{id}, String.class);
return name;
}

6.9.2 通过NamedParameterJdbcTemplate使用命名参数

org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate 提供了对命名参数的关系。与 org.springframework.jdbc.core.JdbcTemplate 没有继承关系,内部包含一个JdbcTemplate

// 定义namedParameterJdbcTemplate和DAO
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource());
return namedParameterJdbcTemplate;
} @Bean
public NamedJdbcSingerDao namedJdbcSingerDao(){
NamedJdbcSingerDao namedJdbcSingerDao = new NamedJdbcSingerDao();
namedJdbcSingerDao.setNamedParameterJdbcTemplate(namedParameterJdbcTemplate());
return namedJdbcSingerDao;
} // 使用namedParameterJdbcTemplate
@Override
public String findNameById(Long id) {
String sql = "select first_name || ' ' || last_name from singer where id = :singerId"; Map<String, Object> namedParams = new HashMap<>();
namedParams.put("singerId", id); return namedParameterJdbcTemplate.queryForObject(sql, namedParams, String.class);
}

6.9.3 使用RowMapper检索域对象

Spring的 org.springframework.jdbc.core.RowMapper<T> 提供了一种简单的方法来完成从JDBC结果集到POJO的映射。

// 手动创建RowMapper
@Override
public List<Singer> findAll() {
String sql = "select id, first_name, last_name, birth_date from singer";
return namedParameterJdbcTemplate.query(sql, new SingerMapper());
} private class SingerMapper implements RowMapper<Singer> {
@Override
public Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Singer().setId(rs.getLong("id")).setFirstName(rs.getString("first_name")).setLastName(rs.getString("last_name")).setBirthDate(rs.getDate("birth_date"));
}
} // 使用Lambda表达式创建匿名RowMapper
@Override
public List<Singer> findAll() {
String sql = "select id, first_name, last_name, birth_date from singer";
return namedParameterJdbcTemplate.query(sql, (rs, rowNum) ->
new Singer().setId(rs.getLong("id")).setFirstName(rs.getString("first_name")).setLastName(rs.getString("last_name")).setBirthDate(rs.getDate("birth_date"))
);
}

6.10 使用ResultSetExtractor检索嵌套域对象

org.springframework.jdbc.core.RowMapper<T> 仅适用于将行映射到单个域对象;对于更复杂的对象结构,则需要使用org.springframework.jdbc.core.ResultSetExtractor 接口。

public List<Singer> findAllWithAlbums() {
String sql = "select s.id, s.first_name, s.last_name, s.birth_date" +
", a.id as album_id, a.title, a.release_date " +
" from singer s left join album a on s.id = a.singer_id";
// return namedParameterJdbcTemplate.query(sql, new SingerWithDetailExtractor());
return namedParameterJdbcTemplate.query(sql, rs -> {
Map<Long, Singer> map = new HashMap<>();
Singer singer;
while (rs.next()) {
Long id = rs.getLong("id");
singer = map.get(id);
if (singer == null) {
singer = new Singer();
singer.setId(id);
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
singer.setAlbums(new ArrayList<>());
map.put(id, singer);
} Long albumId = rs.getLong("album_id");
if (albumId > 0) {
Album album = new Album();
album.setId(albumId);
album.setSingerId(id);
album.setTitle(rs.getString("title"));
album.setReleaseDate(rs.getDate("release_date"));
singer.addAlbum(album);
}
}
return new ArrayList<>(map.values());
});
}

6.11 建模JDBC操作的Spring类

Spring提供了许多有用的类来模拟JDBC数据库,从而让开发人员以更面向对象的方式将ResultSet中的查询和转换逻辑维护到域对象。

  • org.springframework.jdbc.object.MappingSqlQuery<T>

    允许将查询字符串和mapRow()方法一起封装到要给类中
  • org.springframework.jdbc.object.SqlUpdate

    能够封装任何SQL更新语句,绑定SQL参数,在插入新的记录后检索RDBMS生成的键等。
  • org.springframework.jdbc.object.BatchSqlUpdate

    允许执行批量更新操作。可以随时设置批量大小并刷新操作
  • org.springframework.jdbc.object.SqlFunction

    允许使用参数和返回类型调用数据库中的存储函数。此外,还可以使用另一个类StoredProcedure来帮助调用存储过程。

6.12 使用MappingSqlQuery查询数据

Spring提供了MappingSqlQuery<T>类对查询操作进行建模。

示例:

  1. 查询不带参数:
//----定义MappingSqlQuery-------------------------------------//
public class SelectAllSingers extends MappingSqlQuery<Singer> {
private static String SQL_SELECT_ALL_SINGER = "select id, first_name, last_name, birth_date from singer"; public SelectAllSingers(DataSource ds) {
super(ds, SQL_SELECT_ALL_SINGER);
} @Override
protected Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
Singer singer = new Singer();
singer.setId(rs.getLong("id"));
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
return singer;
}
} //----调用--------------------------------------------------//
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.selectAllSingers = new SelectAllSingers(dataSource);
}
@Override
public List<Singer> findAll() {
return selectAllSingers.execute();
}
  1. 查询带参数:
//----定义MappingSqlQuery-------------------------------------//
public class SelectAllSingers extends MappingSqlQuery<Singer> { private static String SQL_FIND_BY_FIRST_NAME = "select id, first_name, last_name, birth_date from singer " +
" where first_name = :first_name"; public SelectAllSingers(DataSource ds) {
super(ds, SQL_FIND_BY_FIRST_NAME);
super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
} @Override
protected Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
Singer singer = new Singer();
singer.setId(rs.getLong("id"));
singer.setFirstName(rs.getString("first_name"));
singer.setLastName(rs.getString("last_name"));
singer.setBirthDate(rs.getDate("birth_date"));
return singer;
}
} //----调用--------------------------------------------------//
public List<Singer> findByFirstName(String firstName) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("first_name", firstName);
return selectAllSingers.executeByNamedParam(paramMap);
}

MappingSqlQuery仅适用于将单个行映射到域对象。对于嵌套对象,则需要将JdbcTemplateResultSetExtractor一起使用。

使用SqlUpdate更新数据

//-------定义SqlUpdate-------------------//
public class UpdateSinger extends SqlUpdate {
private static String SQL_UPDATE_SINGER = "update singer set first_name=:first_name, last_name=:last_name, birth_date=:birth_date where id=:id"; public UpdateSinger(DataSource ds) {
super(ds, SQL_UPDATE_SINGER);
super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("last_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("birth_date", Types.DATE));
super.declareParameter(new SqlParameter("id", Types.INTEGER));
}
} //-------调用-------------------//
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.updateSinger = new UpdateSinger(dataSource);
} public void update(Singer singer) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("first_name", singer.getFirstName());
paramMap.put("last_name", singer.getLastName());
paramMap.put("birth_date", singer.getBirthDate());
paramMap.put("id", singer.getId());
updateSinger.updateByNamedParam(paramMap);
}

6.13 插入数据并检索生成的键

从JDBC 3.0开始,允许以统一的方式检索RDBMS生成的键。

//-----------定义SqlUpdate ------------------------//
public class InsertSinger extends SqlUpdate {
private static final String SQL_INSERT_SINGER = "insert into singer (first_name, last_name, birth_date) values " +
" (:first_name, :last_name, :birth_date)"; public InsertSinger(DataSource ds) {
super(ds, SQL_INSERT_SINGER);
super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("last_name", Types.VARCHAR));
super.declareParameter(new SqlParameter("birth_date", Types.DATE)); super.setGeneratedKeysColumnNames(new String[]{"id"});
super.setReturnGeneratedKeys(true);
}
} //-----------调用 ------------------------//
public void insert(Singer singer) {
System.out.println(singer); Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("first_name", singer.getFirstName());
paramMap.put("last_name", singer.getLastName());
paramMap.put("birth_date", singer.getBirthDate()); GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
insertSinger.updateByNamedParam(paramMap, keyHolder);
singer.setId(keyHolder.getKey().longValue()); System.out.println(singer);
}

6.14 使用BatchSqlUpdate进行批处理操作

//-----------定义BatchSqlUpdate ------------------------//
public class InsertSingerAlbum extends BatchSqlUpdate {
private static final String SQL_INSERT_SINGER_ALBUM = "insert into album (singer_id, title, release_date) values " +
" (?, ?, ?)"; private static final int BATCH_SIZE = 1; public InsertSingerAlbum(DataSource ds) {
super(ds, SQL_INSERT_SINGER_ALBUM); declareParameter(new SqlParameter("singer_id", Types.VARCHAR));
declareParameter(new SqlParameter("title", Types.VARCHAR));
declareParameter(new SqlParameter("release_date", Types.DATE)); setBatchSize(BATCH_SIZE);
}
} //-----------调用 ------------------------//
@Override
public void insertWithDetail(Singer singer) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("first_name", singer.getFirstName());
paramMap.put("last_name", singer.getLastName());
paramMap.put("birth_date", singer.getBirthDate()); GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
insertSinger.updateByNamedParam(paramMap, keyHolder); singer.setId(keyHolder.getKey().longValue()); List<Album> albums = singer.getAlbums();
if (albums != null) {
for (Album album : albums) {
insertSingerAlbum.update(new Object[]{singer.getId(), album.getTitle(), album.getReleaseDate()});
}
}
insertSingerAlbum.flush();
}

6.15 使用SqlFunction调用存储函数

//-----------定义SqlFunction------------------------//
public class StoredFunctionFirstNameById extends SqlFunction<String> {
private static final String SQL = "select getfirstnamebyid(?)"; public StoredFunctionFirstNameById(DataSource dataSource){
super(dataSource,SQL); declareParameter(new SqlParameter(Types.INTEGER));
compile();
}
} //-----------调用------------------------//
public String findFirstNameById(Long id) {
List<String> result = storedFunctionFirstNameById.execute(id);
return result.get(0);
}

Spring还提供了StoredProcedure来调用复杂的存储过程。

6.16 Spring Data项目:JDBC Extensions

Spring创建了Spring Data项目,主要目标是在Spring的核心数据访问功能之上提供有用的扩展,以便与传统RDBMS之外的数据库进行交互。

Spring Data的扩展之一 JDBC Extensions 提供了一些高级功能:

  • QueryDSL支持;
  • 对Oracle数据库的高级支持;

6.17 使用JDBC的注意事项

在JDBC基础上有很多开源库,帮助缩小关系数据结构与Java的OO模型之间的差距。

在使用Spring时,可以混合搭配不同的数据访问技术。例如,可以将Hibernate用作主ORM,然后将JDBC用作一些复杂查询逻辑或批处理操作的补充;可以在单个事务操作中将它们混合搭配,并封装在同一个事务中。

6.18 Spring Boot JDBC

Spring Boot JDBC的启动器:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

启动器使用 HikariCPcom.zaxxer.hikari.HikariDataSource 来配置DataSource bean。老版本可能使用其他不同的DataSource。

Spring Boot还会自动注册以下bean:

  • JdbcTemplate
  • NamedParameterJdbcTemplate
  • PlatformTransactionManagerDataSourceTransactionManager

Spring Boot对JDBC的默认配置:

  1. 启动器默认使用嵌入式数据库。默认schema.sql包含DDL语句,data.sql包含DML,可配置:
// 默认位于classpath下
spring:
datasource:
schema: classpath:db/h2/schema.sql
data: classpath:db/h2/test_data.sqlspring.data
  1. 默认会在启动时初始化数据库,可通过spring.datasource.initialize = false来进行更改;

20191105 《Spring5高级编程》笔记-第6章的更多相关文章

  1. C#高级编程笔记之第二章:核心C#

    变量的初始化和作用域 C#的预定义数据类型 流控制 枚举 名称空间 预处理命令 C#编程的推荐规则和约定 变量的初始化和作用域 初始化 C#有两个方法可以一确保变量在使用前进行了初始化: 变量是字段, ...

  2. C#高级编程笔记之第一章:.NET体系结构

    1.1 C#与.NET的关系 C#不能孤立地使用,必须与.NET Framework一起使用一起考虑. (1)C#的体系结构和方法论反映了.NET基础方法论. (2)多数情况下,C#的特定语言功能取决 ...

  3. 20191105 《Spring5高级编程》笔记-【目录】

    背景 开始时间:2019/09/18 21:30 Spring5高级编程 版次:2019-01-01(第5版) Spring5最新版本:5.1.9 CURRENT GA 官方文档 Spring Fra ...

  4. 读《C#高级编程》第1章问题

    读<C#高级编程>第1章 .Net机构体系笔记 网红的话:爸爸说我将来会是一个牛逼的程序员,因为我有一个梦,虽然脑壳笨但是做事情很能坚持. 本章主要是了解.Net的结构,都是一些概念,并没 ...

  5. Android高级编程笔记(四)深入探讨Activity(转)

    在应用程序中至少包含一个用来处理应用程序的主UI功能的主界面屏幕.这个主界面一般由多个Fragment组成,并由一组次要Activity支持.要在屏幕之间切换,就必须要启动一个新的Activity.一 ...

  6. C#高级编程9 第18章 部署

    C#高级编程9 第18章 部署 使用 XCopy 进行部署 本主题演示如何通过将应用程序文件从一台计算机复制到另一台计算机来部署应用程序. 1.将项目中生成的程序集复制到目标计算机,生成的程序集位于项 ...

  7. C#高级编程9 第17章 使用VS2013-C#特性

    C#高级编程9 第17章 使用VS2013 编辑定位到 如果默认勾选了这项,请去掉勾选,因为勾选之后解决方案的目录会根据当前文件选中. 可以设置项目并行生成数 版本控制软件设置 所有文本编辑器行号显示 ...

  8. C#高级编程9 第16章 错误和异常

    C#高级编程9 第16章 错误和异常 了解这章可以学会如何处理系统异常以及错误信息. System.Exception类是.NET运行库抛出的异常,可以继承它定义自己的异常类. try块代码包含的代码 ...

  9. C#高级编程笔记之第三章:对象和类型

    类和结构的区别 类成员 匿名类型 结构 弱引用 部分类 Object类,其他类都从该类派生而来 扩展方法 3.2 类和结构 类与结构的区别是它们在内存中的存储方式.访问方式(类似存储在堆上的引用类型, ...

  10. UNIX环境高级编程笔记之文件I/O

    一.总结 在写之前,先唠几句,<UNIX环境高级编程>,简称APUE,这本书简直是本神书,像我这种小白,基本上每看完一章都是“哇”这种很吃惊的表情.其实大概三年前,那会大三,我就买了这本书 ...

随机推荐

  1. nginx之热部署,以及版本回滚

    热部署的概念:当从老版本替换为新版本的nginx的时候,如果不热部署的话,会需要取消nginx服务并重启服务才能替换成功,这样的话会使正在访问的用户在断开连接,所以为了不影响用户的体验,且需要版本升级 ...

  2. 07java进阶——集合框架3(Map)

    1.映射表(Map) 1.1基本概念 1.2Map中常用的方法 package cn.jxufe.java.chapter7; import java.util.HashMap; import jav ...

  3. 【ARC101F】Robots and Exits 树状数组优化DP

    ARC101F Robots and Exits 树状数组 有 $ n $ 个机器人和 $ m $ 个出口.这 $ n $ 个机器人的初始位置是 $ a_1,a_2.....a_n $ ,这 $ m ...

  4. 前端之JavaScript:JS简单介绍

    JavaScript(JS)之简单介绍 一.JavaScript的历史 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中).后将其改名Scr ...

  5. JOI2019 有趣的家庭菜园3

    问题描述 家庭菜园专家 JOI 先生在他的家庭菜园中种植了一种叫 Joy 草的植物.在他的菜园里,有 N 个花盆自东向西摆放,编号分别为 \(1, \ldots, N\).每个花盆中有一株 Joy 草 ...

  6. css 上下居中的广法

    方法1 .text{ text-align:center; font-size:0; } .text span{ vertical-align:middle; display:inline-block ...

  7. 【JavaScript】 命名空间污染解决

    闭包解决命名空间污染问题 var init = (function () { var name = "zhangsan", age = 12, sex = "male&q ...

  8. Python 面试问题

    Python 面试问题 最近正在团队内部普及 Python 语言,有些刚接触 Python 语言的工程师在概念上有很多混淆的地方,刚好看到这篇文章:Python面试问题,里面列举的问题都是关于 Pyt ...

  9. div写表格,原生滚动条,数据能够自动滚动

    如何让表格的滚动条能够自动滚动呢? html: <div class="tabinner5"> <div class="tab5 tab5a" ...

  10. Bugku web 计算器

    计算器 打开网页,想输入正确的计算结果发现只输进去一位数??? 遇事不决先F12看一眼源码,发现flag