本文是基于乐字节的秒杀系统总结出来的笔记,纯属个人兴趣,视频原文链接:视频链接
项目源码地址:项目地址
系统介绍
秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀然而,这对于我们系统而言是一个巨大的考验。那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高维度出发,从整体上思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。
其实,秒杀的整体架构可以概括为“稳、准、快”几个关键字。就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。然后就是“准”,就是秒杀 10 台 iPhone,那就只能成交 10 台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以“准”就是要求保证数据的一致性。最后再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。
所以从技术角度上看“稳、准、快”,就对应了我们架构上的高可用、一致性和高性能的要求。
- 高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。对应的方案比如动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化。
- 一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。
- 高可用。 现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。
前置介绍
- 基于Springboot+MybatisPlus+Redis+Rabitmq的高并发商品秒杀系统。
- MybatisPlus:MyBatis-Plus简称MP,是国内的针对MyBatis制作的一个增强框架,对原生MyBatis无侵入,只做增强,目的是可以简化简单的CRUD操作,提高开发效率。简单的CRUD完全不需要写SQL语句,也不必编写持久层接口,仅仅需要继承JpaRepository接口即可。
- Redis是一个高速缓存数据库,是一种key-value(键值对)形式的存储系统,非关系型数据库。Redis的数据 是放在内存里的,所以读写会很快,Redis才能实现持久化(两种实现方式)。
- RabbitMQ是由Erlang语言开发,基于AMQP协议(Advanced Message Queuing Protocol 高级消息队列协议)实现的消息队列,它是一种应用程序之间的通信方法,消息队列在实际开发应用中有着非常广泛的使用。
环境搭建
1.依赖注入
首先,在idea新建一个java项目,利用spring Initializr创建一个spring工程,在依赖那里勾选web、thymeleaf、mysql、lombok等依赖。
<!-- thymeleaf组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok 依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.修改配置文件application.yml
spring:
#thymeleaf配置
thymeleaf:
#关闭缓存
cache: false
prefix: classpath:/static/web/
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接的自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 180000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1
前期开发准备
1.逆向工程
逆向工程简单来说就是,我们先创建好了数据库,然后根据数据库使用Mybatis-Plus的生成器代码自动帮我们生成我们需要的类:controller、service、mapper等等,不需要我们自己再手动配置,省去了不少麻烦。
首先添加Mybatis-Plus依赖和代码生成器依赖,这两个依赖都可以在Mybatis-Plus官网中找到。由于此代码生成器使用的是freemarker模板引擎,因此也需要将freemarker的模板引擎的依赖一并导入。
1.1 依赖注入
<!-- mybatis-plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!-- 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
<!-- freemarker模板引擎 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
1.2 修改配置文件application.yml
在配置文件中添加Mybatis-plus配置信息
#Mybatis-plus配置
mybatis-plus:
#配置Mapper映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
#配置MyBatis数据返回类型别名(默认别名是类名)
type-aliases-package: com.yang.seckill.pojo
#Mybatis SQL 打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
level:
com.yang.seckill.mapper: debug
1.3 代码生成器
public class CodeGenerator {
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
//作者
gc.setAuthor("yang");
//打开输出目录
gc.setOpen(false);
//xml开启 BaseResultMap
gc.setBaseResultMap(true);
//xml 开启BaseColumnList
gc.setBaseColumnList(true);
//日期格式,采用Date
gc.setDateType(DateType.ONLY_DATE);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia" +"/Shanghai");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.yang.seckill")
.setEntity("pojo")
.setMapper("mapper")
.setService("service")
.setServiceImpl("service.impl")
.setController("controller");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
Map<String,Object> map = new HashMap<>();
map.put("date1","1.0.0");
this.setMap(map);
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" +
tableInfo.getEntityName() + "Mapper"
+ StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig()
.setEntity("templates/entity.java")
.setMapper("templates/mapper.java")
.setService("templates/service.java")
.setServiceImpl("templates/serviceImpl.java")
.setController("templates/controller.java");
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
//strategy.setRestControllerStyle(true);
// 公共父类
// strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
//strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("t_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
2.数据库创建
- 数据库创建
create database seckill;
- 用户表
CREATE TABLE t_user(
`id` BIGINT(20) NOT NULL COMMENT '用户ID shoujihaoma',
`nickname` VARCHAR(255) not NULL,
`pasword` VARCHAR(32) DEFAULT NULL COMMENT 'MD5二次加密',
`slat` VARCHAR(10) DEFAULT NULL,
`head` VARCHAR(128) DEFAULT NULL COMMENT '头像',
`register_date` datetime DEFAULT NULL COMMENT '注册时间',
`last_login_date` datetime DEFAULT NULL COMMENT '最后一次登录时间',
`login_count` int(11) DEFAULT '0' COMMENT '登录次数',
PRIMARY KEY(`id`)
);
- 商品表
create table `t_goods`(
`id` BIGINT(20) not null AUTO_INCREMENT COMMENT '商品id',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名称',
`goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题',
`goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品图片',
`goods_detail` LONGTEXT COMMENT '商品描述',
`goods_price` DECIMAL(10, 2) DEFAULT '0.00' COMMENT '商品价格',
`goods_stock` INT(11) DEFAULT '0' COMMENT '商品库存,-1表示没有限制',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8mb4;
- 订单表
CREATE TABLE `t_order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID',
`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
`delivery_addr_id` BIGINT(20) DEFAULT NULL COMMENT '收获地址ID',
`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '商品名字',
`goods_count` INT(20) DEFAULT '0' COMMENT '商品数量',
`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品价格',
`order_channel` TINYINT(4) DEFAULT '0' COMMENT '1 pc,2 android, 3 ios',
`status` TINYINT(4) DEFAULT '0' COMMENT '订单状态,0新建未支付,1已支付,2已发货,3已收货,4已退货,5已完成',
`create_date` datetime DEFAULT NULL COMMENT '订单创建时间',
`pay_date` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=12 DEFAULT CHARSET = utf8mb4;
- 秒杀商品表
CREATE TABLE `t_seckill_goods`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
`seckill_price` DECIMAL(10,2) NOT NULL COMMENT '秒杀家',
`stock_count` INT(10) NOT NULL COMMENT '库存数量',
`start_date` datetime NOT NULL COMMENT '秒杀开始时间',
`end_date` datetime NOT NULL COMMENT '秒杀结束时间',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=3 DEFAULT CHARSET = utf8mb4;
- 秒杀订单表
CREATE TABLE `t_seckill_order` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '秒杀订单ID',
`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
`order_id` BIGINT(20) NOT NULL COMMENT '订单ID',
`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
PRIMARY KEY(`id`)
)ENGINE = INNODB AUTO_INCREMENT=3 DEFAULT CHARSET = utf8mb4;
3.工具类
3.1 2次MD5加密
为了提高用户密码的安全性,密码从前端传入后端,再从后端传入数据库的过程中要经历两次加密过程。此项目中,前端页面js代码已经写好了,在前端已经历一次加密过程,在后端的代码也有两次加密过程,只不过第一次加密的方式与前端代码的加密方式相同,此处只是为了测试加密的准确性,第二次加密是将前端传入后端的密码再次进行加密的过程,此密码将存入数据库,并保留加密的盐值,为后续的密码验证作铺垫。在正式生成中,盐值往往是随机生成的,此项目仅作为学习为主,因此设置所以的盐值为静态盐值。
使用MD5加密,首先要注入相关的依赖:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
2次MD5加密:
/**
* MD5工具类
*/
@Component
public class MD5Util {
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
public static final String salt = "1a2b3c4d";
/**
* 第一次加密:前端输入的密码加密转为后端密码
* @param inputPass 前端输入的密码
* @return 第一次加密后的密码
*/
public static String inputPassToBackendPass(String inputPass){
String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4) ;
return md5(str);
}
/**
* 第二次加密:后端加密后的密码再次加密存入数据库
* @param backendPass 后端加密后的密码
* @param salt 存入数据库的盐值
* @return 第二次加密后的密码
*/
public static String backendPassToDBPass(String backendPass, String salt){
String str = "" + salt.charAt(0) + salt.charAt(2) + backendPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
/**
* 直接前端输入的密码经过2次加密后存入数据库的密码
* @param inputPass 前端输入的密码
* @param salt 存入数据库的盐值
* @return 存入数据库的密码
*/
public static String inputPassToDBPass(String inputPass, String salt){
String backendPass = inputPassToBackendPass(inputPass);
String dbPass = backendPassToDBPass(backendPass,salt);
return dbPass;
}
public static void main(String[] args) {
System.out.println("前端进行加密后的密码:" + inputPassToBackendPass("123456"));
System.out.println("后端传入数据库进行加密后的密码:" + backendPassToDBPass(inputPassToBackendPass("123456"),"1a2b3c4d"));
System.out.println("经过2次加密后的密码:" + inputPassToDBPass("123456","1a2b3c4d"));
}
}
3.2 电话号码校验类
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");
public static boolean isMobile(String mobile){
if (StringUtils.isEmpty(mobile)){
return false;
}
Matcher matcher = mobile_pattern.matcher(mobile);
return matcher.matches();
}
}
3.3 异常枚举
为什么会出现异常?比如,用户在进行注册时可能会产生用户名被占用的错误,这时需要抛出一个异常。
- 公共返回对象枚举
创建一个公共返回对象枚举,列出项目测试过程中可能遇到的异常情况,因项目刚开始,所以只罗列了登录过程中可能出现的异常,后续业务中所出现的异常将后续添加。
/**
* 公共返回对象枚举
*/
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
//通用
SUCCESS(200,"SUCCESS"),
ERROR(500,"服务端异常"),
//登录模块
LOGIN_ERROR(500218,"用户名或密码错误"),
MOBILE_ERROR(500211,"手机号码格式错误"),
//绑定异常
BIND_ERROR(500212,"参数校验异常");
private final Integer code;
private final String message;
}
- 公共返回对象
设置公共返回对象成功与失败的方法。
/**
* 公共返回对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
private long code;
private String message;
private Object obj;
/**
* 成功返回结果
* @return
*/
public static RespBean success(){
return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
}
/**
* 成功返回结果
* @return
*/
public static RespBean success(Object obj){
return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBean.success().getMessage(),obj);
}
/**
* 失败返回结果
* @return
*/
public static RespBean error(RespBeanEnum respBeanEnum){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
}
/**
* 失败返回结果
* @return
*/
public static RespBean error(RespBeanEnum respBeanEnum, Object obj){
return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
}
}
3.4 生成用户工具类
在后面进行压力测试的时候,要生成很多用户对接口进行访问,此工具类能够按照生成规则生成大量用户信息:
/**
* 生成用户工具类
*/
public class UserUtil {
private static void createUser(int count) throws Exception {
List<User> users = new ArrayList<>(count);
//生成用户
for (int i = 0; i < count; i++) {
User user = new User();
user.setId(13000000000L + i);
user.setLoginCount(1);
user.setNickname("user" + i);
user.setRegisterDate(new Date());
user.setSlat("1a2b3c4d");
user.setPassword(MD5Util.inputPassToDBPass("123456", user.getSlat()));
users.add(user);
}
System.out.println("create user");
// //插入数据库
Connection conn = getConn();
String sql = "insert into t_user(login_count, nickname, register_date, slat, password, id)values(?,?,?,?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
pstmt.setInt(1, user.getLoginCount());
pstmt.setString(2, user.getNickname());
pstmt.setTimestamp(3, new Timestamp(user.getRegisterDate().getTime()));
pstmt.setString(4, user.getSlat());
pstmt.setString(5, user.getPassword());
pstmt.setLong(6, user.getId());
pstmt.addBatch();
}
pstmt.executeBatch();
pstmt.close();
conn.close();
System.out.println("insert to db");
//登录,生成userTicket
String urlString = "http://localhost:8080/login/doLogin";
File file = new File("C:\\Users\\Administrator\\Desktop\\config.txt");
if (file.exists()) {
file.delete();
}
RandomAccessFile raf = new RandomAccessFile(file, "rw");
file.createNewFile();
raf.seek(0);
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
URL url = new URL(urlString);
HttpURLConnection co = (HttpURLConnection) url.openConnection();
co.setRequestMethod("POST");
co.setDoOutput(true);
OutputStream out = co.getOutputStream();
String params = "mobile=" + user.getId() + "&password=" + MD5Util.inputPassToBackendPass("123456");
out.write(params.getBytes());
out.flush();
InputStream inputStream = co.getInputStream();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len = 0;
while ((len = inputStream.read(buff)) >= 0) {
bout.write(buff, 0, len);
}
inputStream.close();
bout.close();
String response = new String(bout.toByteArray());
ObjectMapper mapper = new ObjectMapper();
RespBean respBean = mapper.readValue(response, RespBean.class);
String userTicket = ((String) respBean.getObj());
System.out.println("create userTicket : " + user.getId());
String row = user.getId() + "," + userTicket;
raf.seek(raf.length());
raf.write(row.getBytes());
raf.write("\r\n".getBytes());
System.out.println("write to file : " + user.getId());
}
raf.close();
System.out.println("over");
}
private static Connection getConn() throws Exception {
String url = "jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "root";
String driver = "com.mysql.cj.jdbc.Driver";
Class.forName(driver);
return DriverManager.getConnection(url, username, password);
}
public static void main(String[] args) throws Exception {
createUser(500);
}
}
3.5 Json工具类
Json工具类内部有众多方法,有将对象转换成json字符串、将字符串转换为对象等方法:
/**
* Json工具类
*/
public class JsonUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
/**
* 将对象转换成json字符串
*
* @param obj
* @return
*/
public static String object2JsonStr(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
//打印异常信息
e.printStackTrace();
}
return null;
}
/**
* 将字符串转换为对象
*
* @param <T> 泛型
*/
public static <T> T jsonStr2Object(String jsonStr, Class<T> clazz) {
try {
return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
*
* @param jsonStr
* @param beanType
* @return
*/
public static <T> List<T> jsonToList(String jsonStr, Class<T> beanType) {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = objectMapper.readValue(jsonStr, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();