文章关注是架构设计和一些以前学习时没有理解的点,具体代码需参考慕课网相关教程。
一、Service接口和实现类
在J2EE工程中,service层一般负责接收servlet从前端获取的数据,并进行数据的初步处理(组装成查询条件),将其扔给DAO层去处理,得到的结果交到servlet中。由servlet返回给前端,利用c标签、JQuery、Ajax等进行数据展示处理。
service包和serviceImpl包:对应接口定义、接口实现。
提出几个问题,复习完项目代码后进行回答:
1. 定义接口时,该如何去考虑方法需要什么参数?
站在接口调用者的角度去设计接口,接口需要的参数应该简单直白,尽量避免扔一个map进去,然后key-value取值。返回类型同理。例如,秒杀系统只在规定的时间暴露出秒杀接口的链接,因此本程序设计了一个DTO对象exposer,封装了待暴露的秒杀接口,而不是采用一个map然后让秒杀接口地址作为map的一个key。
/**
* 暴露秒杀地址DTO
*
*/
public class Exposer {
// 是否开启秒杀
private boolean exposed;
// 一种加密措施
private String md5;
// id
private long seckillId;
// 系统当前时间(毫秒)
private long now;
// 开启时间
private long start;
// 结束时间
private long end;
public Exposer(boolean exposed, String md5, long seckillId) {
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
public Exposer(boolean exposed, long seckillId, long now, long start,
long end) {
this.exposed = exposed;
this.seckillId = seckillId;
this.now = now;
this.start = start;
this.end = end;
}
public Exposer(boolean exposed, long seckillId) {
this.exposed = exposed;
this.seckillId = seckillId;
}
@Transactional
public SeckillExecution excuteSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {
if (md5 == null && !md5.equals(getMD5(seckillId))) {
throw new SeckillException("seckill data rewrite");
}
// 执行秒杀逻辑:减库存+记录购买行为
Date nowTime = new Date();
try {
// 记录购买行为
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
// 数据库中的唯一记录:(seckillId, userPhone)
if (insertCount <= 0) {
// 重复秒杀
throw new RepeatKillException("seckill repeated");
} else {
// 减库存,热点商品竞争(rowLock出现的地方)
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
// 没有更新记录,秒杀结束,rollback
throw new SeckillCloseException("seckill is closed");
} else {
// 秒杀成功,commit
SuccessKilled secKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
// 使用秒杀成功的构造函数
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, secKilled);
}
}
} catch (SeckillCloseException e1) {
throw e1;
} catch (RepeatKillException e2) {
throw e2;
} catch (Exception e) {
logger.error(e.getMessage(), e);
// 所有编译期异常 转化为运行期异常
throw new SeckillException("seckill inner error:" + e.getMessage());
}
}
<context:component-scan base-package="org.seckill.service">context:component-scan>
2)用注解的方式将Service的实现类加入到Spring IOC容器中:
@Service
public class SeckillServiceImpl implements SeckillService {...}
二、DTO层设计
DTO包:数据传输层对象(和entity区别,entity是和业务数据库存储相关的,而dto关注web和service之间的数据传递)
三、异常exception设计
自定义异常exception包:封装和业务相关的具体异常,如重复秒杀异常、秒杀关闭异常等。和Spring的事务结合使用。
Java的异常分为两大类:Checked异常和Runtime异常。除了RuntimeException类和其子类的实例之外,别的异常都称为Checked异常。Spring的事务管理处理的是Runtime异常,所以在打了@Transactional注解的方法体,要把Checked异常捕获之后,主动throw new RuntimeException(实际上是继承RuntimeException类的某个自定义异常类)。
四、枚举类的应用
程序一般都会用自己的数据字典,数据字典可放在枚举当中,在代码里不要用中文硬编码。便于管理,保持代码整洁。
public enum SeckillStatEnum {
SUCCESS(1, "秒杀成功"), END(0, "秒杀结束"), REPEAT_KILL(-1, "重复秒杀"), INNER_ERROR(
-2, "系统异常"), DATA_REWRITE(-3, "数据篡改");
private int state;
private String stateInfo;
private SeckillStatEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
}
五、Spring声明式事务
1)spring配置文件中写
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
bean>
<tx:annotation-driven transaction-manager="transactionManager" />
2)service实现类的方法中:
@Transactional
/**
* 使用注解控制事务方法的优点: 1:开发团队达成一致约定,明确标注事务方法的编程风格。
* 2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作,RPC(操作缓存)/HTTP请求或者剥离到事务方法外部.
* 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制.(参考MySql行级锁)
*/
public SeckillExecution excuteSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {...}
六、Service层的Junit的集成测试
在需要测试的类名上,new-other-junit,生成对应的测试类。编写测试方法。
七、额外的知识补充——MySQL的行级锁