Transaction 在 Controller 层的探索
来源:http://www.importnew.com/28465.html
一般开发中事务要求我们放在Service层,可是有些情况,我们可能会要求放在Controller层,你有没有碰到过这样的需求呢?那么放到Controller层事务会生效吗?会产生什么问题呢?下面一起来看看
I、透过现象看本质
第一种情况
Controller层代码如下
@RestController
@RequestMapping("/city")
public class CityControllerImpl implements CityController {
@Autowired
private CityService cityService;
@Override
@RequestMapping(value = "getCity",method = RequestMethod.GET,produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Transcational
public BaseResult<City> getCity(@RequestParam("id") Integer id) {
City one = cityService.getOne(id);
BaseResult<City> baseResult=new BaseResult<>();
baseResult.setData(one);
return baseResult;
}
}
运行结果
对的,你没有看错,当Transactional加载Controller层时出现404异常
第二种情况
Controller层代码如下
@RestController
@RequestMapping("/city")
public class CityControllerImpl {
@Autowired
private CityService cityService;
@RequestMapping(value = "getCity",method = RequestMethod.GET,produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Transactional
public BaseResult<City> getCity(@RequestParam("id") Integer id) {
City one = cityService.getOne(id);
BaseResult<City> baseResult=new BaseResult<>();
baseResult.setData(one);
return baseResult;
}
}
跟上面的区别,就是没有实现CityController接口了,那么我们运行一下,会有什么结果呢?
运行结果如下:
{
data: null,
message: null,
status: 0
}
第二种情况居然没有啥问题,那么Transactional是否正常回滚呢?这里答案我直接告诉大家了,即使是换成有数据更改的接口,我们的事务是生效的。
下面我为大家看源码解释一下
第三种情况
笔者测试使用支持==JAX-RS 2.0==的 Resteasy 测试,发现是没有这个问题的,大家可以自测一下Jersey是不是存在这个问题,推断应该没有
II、熟悉本质解现象
1. 区别
可以看出,我们两个Controller的区别就是一个有实现接口,一个没有实现,为什么差别会这么大呢?
2. 事务的本质
我们知道事务是基于代理实现的,目前Spring中有JDK动态代理和CGLIB代理两种代理,那么跟Spring选择的代理有没有关系呢?我们看一下Spring在代理类的时候选择使用何种代理的源代码。如下:
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
这是Spring创建代理比较核心的一段代码,在类 DefaultAopProxyFactory
中,不管加没有加接口,Spring看到了@Transactional
注解都会给我们的Controller注册为一个代理对象。注意:Spring并非对所有的Controller都会创建代理类,假如我们的Controller没有暴露任何切面,Spring并不会创建一个代理类
,这里可能大家会感到奇怪,我们这里打个TAG,文末讲解。
继续刚刚的话题,第一种情况,由于我们的Controller有接口,所以就走了JDK代理,相反第二种走了Cglib代理。OK, 我们的CityControllerImpl现在是一个代理类。那么为什么会发生404异常呢?
3. SpringMvc的原理
为什么Controller变成代理之后,就会404异常了,肯定跟我们的SpringMVC有关,我们看一下SpringMVC的核心类 AbstractHandlerMethodMapping
这个类可以绑定URL和需要执行处理器的哪个方法。这个抽象类实现了initializingBean
接口,其实主要的注册URL操作则是通过这个接口的afterPropertiesSet()接口方法来调用的。然后调用initHandlerMethods
方法进行绑定URL。方法详细如下:
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class<?> beanType = null;
try {
beanType = getApplicationContext().getType(beanName);
}
catch (Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
}
handlerMethodsInitialized(getHandlerMethods());
}
beanType中取出来是 CityControllerImpl
代理类,这里大家注意,代码第21行,有一个isHandler
方法,这个方法用于判定这个类是不是Handler,其中代码如下:
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
看到这里相信大家已经很明白了,这里就是看你这个类上面有没有Controller注解和RequestMapping注解。如果有,就建立相关的映射关系(URL->Handler)
其中有接口的是被JDK代理的,生成的是JDK代理类
JDK的动态代理是靠多态和反射来实现的,它生成的代理类需要实现你传入的接口,并通过反射来得到接口的方法对象,并将此方法对象传参给增强类的invoke方法去执行,从而实现了代理功能。
CityController生成的代理类文件如下:
public final class cityControllerImpl extends Proxy implements Proxy86 {
private static Method m1;
private static Method m32;
private static Method m7;
public cityControllerImpl(InvocationHandler var1) throws {
super(var1);
}
public final TargetSource getTargetSource() throws {
try {
return (TargetSource)super.h.invoke(this, m8, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void addAdvice(int var1, Advice var2) throws AopConfigException {
try {
super.h.invoke(this, m21, new Object[]{var1, var2});
} catch (RuntimeException | Error var4) {
throw var4;
} catch (Throwable var5) {
throw new UndeclaredThrowableException(var5);
}
}
public final BaseResult getCity(Integer var1) throws {
try {
return (BaseResult)super.h.invoke(this, m27, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
}
类已经被精简过,我们看到生成的代理类中完全没有
@Controller
@RequestMapping
注解,所以isHandler方法执行失败,所以根本不会加到SpringMvc的控制器处理方法中去,当URL请求过来的时候,找不到对应的处理器处理,所以就报404错误啦-
没有接口的是被CGLIB代理的,生成的是CGlib代理类
CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础
==其实isHandler方法会代理类的接口和父类进行扫描==,看你有没有这个注解,JDK代理中cityControllerImpl接口和父类都没有注解,而CGlib代理的父类是CityControllerImpl
这个原始的类, 所以返回为真
public class CityControllerImpl$$EnhancerBySpringCGLIB$$8cae5808 extends CityControllerImpl implements SpringProxy, Advised, Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
final BaseResult CGLIB$getCity$0(Integer var1) {
return super.getCity(var1);
}
public final BaseResult getCity(Integer var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? (BaseResult)var10000.intercept(this, CGLIB$getCity$0$Method, new Object[]{var1}, CGLIB$getCity$0$Proxy) : super.getCity(var1);
}
}
4. 思考
如果Controller层不加@Transcational
注解的时候,为什么又不会产生404异常呢?其实如果你Controller不加任何织入代码的话(自定义aop切面等,有兴趣的可以用AspectJ试一下织入Controller层的Method方法会发生什么事情),Spring是不会给你的类生成代理的,也就是在AbstractHandlerMethodMapping
绑定的时候,这个类不是一个代理,所以才会匹配成功。
推荐阅读