Spring业务失效的12种场景
一 业务不收效
1.拜访权限问题
众所周知,java的拜访权限首要有四种:private、default、protected、public,它们的权限从左到右,顺次变大。
但假如咱们在开发过程中,把有某些业务办法,界说了过错的拜访权限,就会导致业务功用出问题,例如:
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
咱们可以看到add办法的拜访权限被界说成了private
,这样会导致业务失效,spring要求被署理办法有必要是public
的。
说白了,在AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
办法中有个判别,假如目标办法不是public,则TransactionAttribute
返回null,即不支撑业务。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
也便是说,假如咱们自界说的业务办法(即目标办法),它的拜访权限不是public
,而是private、default或protected的话,spring则不会供给业务功用。
2. 办法用final润饰
有时分,某个办法不想被子类从头,这时可以将该办法界说成final的。一般办法这样界说是没问题的,但假如将业务办法界说成final,例如:
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel){
saveData(userModel);
updateData(userModel);
}
}
咱们可以看到add办法被界说成了final
的,这样会导致业务失效。
为什么?
假如你看过spring业务的源码,或许会知道spring业务底层运用了aop,也便是经过jdk动态署理或者cglib,帮咱们生成了署理类,在署理类中完结的业务功用。
但假如某个办法用final润饰了,那么在它的署理类中,就无法重写该办法,而增加业务功用。
3.办法内部调用
有时分咱们需求在某个Service类的某个办法中,调用别的一个业务办法,比方:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
咱们看到在业务办法add中,直接调用业务办法updateStatus。从前面介绍的内容可以知道,updateStatus办法具有业务的才干是由于spring aop生成署理了目标,可是这种办法直接调用了this目标的办法,所以updateStatus办法不会生成业务。
由此可见,在同一个类中的办法直接内部调用,会导致业务失效。
那么问题来了,假如有些场景,的确想在同一个类的某个办法中,调用它自己的别的一个办法,该怎么办呢?
3.1 新加一个Service办法
这个办法十分简略,只需求新加一个Service办法,把@Transactional注解加到新Service办法上,把需求业务履行的代码移到新办法中。详细代码如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
3.2 在该Service类中注入自己
假如不想再新加一个Service类,在该Service类中注入自己也是一种挑选。详细代码如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
或许有些人或许会有这样的疑问:这种做法会不会呈现循环依靠问题?
答案:不会。
3.3 经过AopContent类
在该Service类中运用AopContext.currentProxy()获取署理目标
上面的办法2的确可以解决问题,可是代码看起来并不直观,还可以经过在该Service类中运用AOPProxy获取署理目标,完结相同的功用。详细代码如下:
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
4.未被spring办理
在咱们平时开发过程中,有个细节很简单被疏忽。即运用spring业务的前提是:目标要被spring办理,需求创立bean实例。
通常状况下,咱们经过@Controller、@Service、@Component、@Repository等注解,可以自动完结bean实例化和依靠注入的功用。
假如有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比方:
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
从上面的比方,咱们可以看到UserService类没有加@Service
注解,那么该类不会交给spring办理,所以它的add办法也不会生成业务。
5.多线程调用
在实践项目开发中,多线程的运用场景仍是挺多的。假如spring业务用在多线程场景中,会有问题吗?
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
从上面的比方中,咱们可以看到业务办法add中,调用了业务办法doOtherThing,可是业务办法doOtherThing是在别的一个线程中调用的。
这样会导致两个办法不在同一个线程中,获取到的数据库衔接不一样,从而是两个不同的业务。假如想doOtherThing办法中抛了反常,add办法也回滚是不或许的。
假如看过spring业务源码的朋友,或许会知道spring的业务是经过数据库衔接来完结的。当时线程中保存了一个map,key是数据源,value是数据库衔接。
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
咱们说的同一个业务,其实是指同一个数据库衔接,只要具有同一个数据库衔接才干一起提交和回滚。假如在不同的线程,拿到的数据库衔接肯定是不一样的,所以是不同的业务。
6.表不支撑业务
周所周知,在mysql5之前,默许的数据库引擎是myisam
。
它的优点就不用多说了:索引文件和数据文件是分隔存储的,对于查多写少的单表操作,性能比innodb更好。
有些老项目中,或许还在用它。
在创立表的时分,只需求把ENGINE
参数设置成MyISAM
即可:
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
myisam好用,但有个很丧命的问题是:不支撑业务
。
假如只是单表操作还好,不会呈现太大的问题。但假如需求跨多张表操作,由于其不支撑业务,数据极有或许会呈现不完整的状况。
此外,myisam还不支撑行锁和外键。
所以在实践业务场景中,myisam运用的并不多。在mysql5今后,myisam现已逐步退出了历史的舞台,取而代之的是innodb。
7.未敞开业务
有时分,业务没有收效的根本原因是没有敞开业务。
你看到这句话或许会觉得好笑。
敞开业务不是一个项目中,最最最基本的功用吗?
为什么还会没有敞开业务?
没错,假如项目现已建立好了,业务功用肯定是有的。
但假如你是在建立项目demo的时分,只要一张表,而这张表的业务没有收效。那么会是什么原因形成的呢?
当然原因有许多,但没有敞开业务,这个原因极其简单被疏忽。
假如你运用的是springboot项目,那么你很走运。由于springboot经过DataSourceTransactionManagerAutoConfiguration
类,现已静静的帮你敞开了业务。
你所要做的事情很简略,只需求装备spring.datasource
相关参数即可。
但假如你运用的仍是传统的spring项目,则需求在applicationContext.xml文件中,手动装备业务相关参数。假如忘了装备,业务肯定是不会收效的。
详细装备如下信息:
<!-- 装备业务办理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 用切点把业务切进去 -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
静静的说一句,假如在pointcut标签中的切入点匹配规矩,配错了的话,有些类的业务也不会收效。
二 业务不回滚
1.过错的传达特性
其实,咱们在运用@Transactional
注解时,是可以指定propagation
参数的。
该参数的作用是指定业务的传达特性,spring目前支撑7种传达特性:
-
REQUIRED
假如当时上下文中存在业务,那么加入该业务,假如不存在业务,创立一个业务,这是默许的传达属性值。 -
SUPPORTS
假如当时上下文存在业务,则支撑业务加入业务,假如不存在业务,则运用非业务的方法履行。 -
MANDATORY
假如当时上下文中存在业务,否则抛出反常。 -
REQUIRES_NEW
每次都会新建一个业务,并且一起将上下文中的业务挂起,履行当时新建业务完结今后,上下文业务康复再履行。 -
NOT_SUPPORTED
假如当时上下文中存在业务,则挂起当时业务,然后新的办法在没有业务的环境中履行。 -
NEVER
假如当时上下文中存在业务,则抛出反常,否则在无业务环境上履行代码。 -
NESTED
假如当时上下文中存在业务,则嵌套业务履行,假如不存在业务,则新建业务。
假如咱们在手动设置propagation参数的时分,把传达特性设置错了,比方:
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
咱们可以看到add办法的业务传达特性界说成了Propagation.NEVER,这种类型的传达特性不支撑业务,假如有业务则会抛反常。
目前只要这三种传达特性才会创立新业务:REQUIRED,REQUIRES_NEW,NESTED。
2.自己吞了反常
业务不会回滚,最常见的问题是:开发者在代码中手动try…catch了反常。比方:
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
这种状况下spring业务当然不会回滚,由于开发者自己捕获了反常,又没有手动抛出,换句话说便是把反常吞掉了。
假如想要spring业务可以正常回滚,有必要抛出它可以处理的反常。假如没有抛反常,则spring以为程序是正常的。
3.手动抛了别的反常
即便开发者没有手动捕获反常,但假如抛的反常不正确,spring业务也不会回滚。
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
上面的这种状况,开发人员自己捕获了反常,又手动抛出了反常:Exception,业务相同不会回滚。
由于spring业务,默许状况下只会回滚RuntimeException
(运行时反常)和Error
(过错),对于一般的Exception(非运行时反常),它不会回滚。
4.自界说了回滚反常
在运用@Transactional注解声明业务时,有时咱们想自界说回滚的反常,spring也是支撑的。可以经过设置rollbackFor
参数,来完结这个功用。
但假如这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:
@Slf4j
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}
}
假如在履行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等反常。而BusinessException是咱们自界说的反常,报错的反常不属于BusinessException,所以业务也不会回滚。
即便rollbackFor有默许值,但阿里巴巴开发者规范中,仍是要求开发者从头指定该参数。
这是为什么呢?
由于假如运用默许值,一旦程序抛出了Exception,业务不会回滚,这会呈现很大的bug。所以,主张一般状况下,将该参数设置成:Exception或Throwable。
5.嵌套业务回滚多了
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
这种状况运用了嵌套的内部业务,原本是希望调用roleService.doOtherThing办法时,假如呈现了反常,只回滚doOtherThing办法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。
由于doOtherThing办法呈现了反常,没有手动捕获,会继续往上抛,到外层add办法的署理办法中捕获了反常。所以,这种状况是直接回滚了整个业务,不只回滚单个保存点。
怎么样才干只回滚保存点呢?
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
可以将内部嵌套业务放在try/catch中,并且不继续往上抛反常。这样就能确保,假如内部嵌套业务中呈现反常,只回滚内部业务,而不影响外部业务。
三 其他
1 大业务问题
在运用spring业务时,有个让人十分头疼的问题,便是大业务问题。
通常状况下,咱们会在办法上@Transactional
注解,填加业务功用,比方:
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}
@Service
public class RoleService {
@Autowired
private RoleService roleService;
@Transactional
public void save(UserModel userModel) throws Exception {
query4();
query5();
query6();
saveData(userModel);
}
}
但@Transactional
注解,假如被加到办法上,有个缺陷便是整个办法都包含在业务傍边了。
上面的这个比方中,在UserService类中,其实只要这两行才需求业务:
roleService.save(userModel);
update(userModel);
在RoleService类中,只要这一行需求业务:
saveData(userModel);
现在的这种写法,会导致一切的query办法也被包含在同一个业务傍边。
假如query办法十分多,调用层级很深,而且有部分查询办法比较耗时的话,会形成整个业务十分耗时,而从形成大业务问题。
2.编程式业务
上面聊的这些内容都是根据@Transactional
注解的,首要说的是它的业务问题,咱们把这种业务叫做:声明式业务
。
其实,spring还供给了别的一种创立业务的方法,即经过手动编写代码完结的业务,咱们把这种业务叫做:编程式业务
。例如:
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
在spring中为了支撑编程式业务,专门供给了一个类:TransactionTemplate,在它的execute办法中,就完结了业务的功用。
相较于@Transactional
注解声明式业务,我更主张我们运用,根据TransactionTemplate
的编程式业务。首要原因如下:
- 避免由于spring aop问题,导致业务失效的问题。
- 可以更小粒度的控制业务的规模,更直观。