事物在Controller层的探索

Transaction在Controller层的探索

一般开发中事务要求我们放在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) {                     
                 // An unresolvable bean type, probably from a lazy bean - let's ignore it.                     
                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的基础

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);
  }
}

==其实isHandler方法会代理类的接口和父类进行扫描==,看你有没有这个注解,JDK代理中cityControllerImpl接口和父类都没有注解,而CGlib代理的父类是CityControllerImpl 这个原始的类, 所以返回为真

4. 思考

如果Controller层不加@Transcational注解的时候,为什么又不会产生404异常呢?其实如果你Controller不加任何织入代码的话(自定义aop切面等,有兴趣的可以用AspectJ试一下织入Controller层的Method方法会发生什么事情),Spring是不会给你的类生成代理的,也就是在AbstractHandlerMethodMapping 绑定的时候,这个类不是一个代理,所以才会匹配成功。

原文地址:

https://blog.csdn.net/u011410529/article/details/79671309

原文发布于微信公众号 - java技术学习之道(javajsxxzd)

原文发表时间:2018-04-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

Mix 10 上的asp.net mvc 2的相关Session

Beyond File | New Company: From Cheesy Sample to Social Platform Scott Hansel...

2607
来自专栏Ceph对象存储方案

Luminous版本PG 分布调优

Luminous版本开始新增的balancer模块在PG分布优化方面效果非常明显,操作也非常简便,强烈推荐各位在集群上线之前进行这一操作,能够极大的提升整个集群...

3205
来自专栏我和未来有约会

Silverlight第三方控件专题

这里我收集整理了目前网上silverlight第三方控件的专题,若果有所遗漏请告知我一下。 名称 简介 截图 telerik 商 RadC...

4045
来自专栏ASP.NETCore

ASP.NET Core 整合Autofac和Castle实现自动AOP拦截

除了ASP.NETCore自带的IOC容器外,我们还可以使用其他成熟的DI框架,如Autofac,StructureMap等(笔者只用过Unity,Ninjec...

674
来自专栏陈仁松博客

ASP.NET Core 'Microsoft.Win32.Registry' 错误修复

今天在发布Asp.net Core应用到Azure的时候出现错误InvalidOperationException: Cannot find compilati...

4878
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.2K7
来自专栏张善友的专栏

Miguel de Icaza 细说 Mix 07大会上的Silverlight和DLR

Mono之父Miguel de Icaza 详细报道微软Mix 07大会上的Silverlight和DLR ,上面还谈到了Mono and Silverligh...

2717
来自专栏芋道源码1024

熔断器 Hystrix 源码解析 —— 断路器 HystrixCircuitBreaker

本文主要基于 Hystrix 1.5.X 版本 1. 概述 2. HystrixCircuitBreaker 3. HystrixCircuitBreaker....

5367
来自专栏杨龙飞前端

scrollto 到指定位置

2534
来自专栏大内老A

The .NET of Tomorrow

Ed Charbeneau(http://developer.telerik.com/featured/the-net-of-tomorrow/) Exciti...

32110

扫码关注云+社区