【问题标题】:Why calling a simple method in SpringMVC service super slow than static method every time?为什么每次调用 Spring MVC 服务中的简单方法都比静态方法慢?
【发布时间】:2019-05-18 09:00:05
【问题描述】:

在 SpringMVC 服务中进入一个方法和退出该方法花费了太多毫秒。但是,如果我将方法修改为静态,则只需 1 毫秒。 我们的项目是一个SpringMVC-Mybatis项目。我发现控制器进入一个服务方法和退出该方法大约需要70毫秒。 该方法从具有 128 个键的静态映射中获取值。

@Controller 
public class OrdersController extends BaseAction
{
    @Autowired(required=false)
    private CompanyService<Company> companyService; 

    public void validateInfo()
    {
            Company company = CompanyService.queryByIdFromCache(account);// cost about 70 milliseconds, but if the method is static it is 1 milliseconds.
    }

}

@Service("companyService")
public class CompanyService<T> extends BaseService<T> 
{

    private static ConcurrentMap<Integer, Company>cache = new ConcurrentHashMap<>(); //map with 128 keys

    public Company queryByIdFromCache(Integer id)
    {
        return cache.get(id);
    }
}

我希望该方法在 2 毫秒内完成。 该服务在单例模式下工作。和 companyService 是同一个实例。 我不想把所有的方法都改成静态方法,因为有些代码必须用非静态的方式调用。

【问题讨论】:

  • 一种可能的解释是 Spring Bean 被创建为代理。因此,他们不能代理static 方法。这意味着如果方法是static,它不会被代理,因此不必被拦截(这会花费额外的时间和资源)。您可以尝试在方法内设置断点,并使用AopUtils.isAopProxy() 或类似方法来验证这一点。
  • 你是如何开始测量的?
  • @Eugene JIT 与确定他的OrdersController 是否被 Spring 代理有什么关系?
  • @user991710,我会试试的。谢谢。
  • @Eugene:对不起,我已经删除了日志。

标签: java spring profiling benchmarking static-methods


【解决方案1】:

很可能是切面拦截了对非静态服务方法的调用。

有几种方法可以分析执行情况并找到服务调用的热点。我使用 NetBeans 的 Test Profiler 重现了这个问题。

首先,我创建了静态(组件)和非静态服务:

@Service
public class DemoService {

    private static final ConcurrentMap<Integer, String> CACHE = new ConcurrentHashMap<>();

    public DemoService() {
        for (int i = 0; i < 128; i++) {
            CACHE.put(i, String.valueOf(i));
        }
    }

    public String queryByIdFromCache(Integer id) {
        return CACHE.getOrDefault(id, "");
    }

}


public class DemoStaticService {

    private static final ConcurrentMap<Integer, String> CACHE = new ConcurrentHashMap<>();


    static {
        for (int i = 0; i < 128; i++) {
            CACHE.put(i, String.valueOf(i));
        }
    }

    public static String queryByIdFromCache(Integer id) {
        return CACHE.getOrDefault(id, "");
    }

}

然后我创建了一个控制器,其中包含两个操作,一个调用非静态注入服务,另一个调用使用静态方法的服务:

@RestController
@RequestMapping(path = "/demo")
public class DemoController {

    @Autowired
    private DemoService demoService;

    @GetMapping(path = "/demo")
    public String callService(@RequestParam Integer id) {
        return demoService.queryByIdFromCache(id);
    }

    @GetMapping(path = "/static-demo")
    public String callStaticService(@RequestParam Integer id) {
        return DemoStaticService.queryByIdFromCache(id);
    }

}

之后,我编写了两个单元测试来帮助我分析服务方法:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoControllerTest {

    @Autowired
    private DemoController demoController;

    @Before
    public void before() {
        demoController.callService(1);
        demoController.callStaticService(1);
    }

    @Test
    public void testCallService() {
        for (int id = 0; id < 128; id++) {
            demoController.callService(id);
        }
    }

    @Test
    public void testCallStaticService() {
        for (int id = 0; id < 128; id++) {
            demoController.callStaticService(id);
        }
    }

}

打开测试文件后,我选择了Profile -&gt; Profile Test File菜单项:

然后从▶ Profile 下拉菜单中,我选择了Methods 选项:

最后我点击了▶ Profile 按钮来分析测试。我得到了这个结果,这表明对注入服务的调用仅比对静态方法的调用贵 50%:

但是如果第二种方法被切面拦截了怎么办(例如@Transactional)?

为了测试这一点,我更新了 DemoService 并使其方法具有事务性

@Service
public class DemoService {

    private static final ConcurrentMap<Integer, String> CACHE = new ConcurrentHashMap<>();

    public DemoService() {
        for (int i = 0; i < 128; i++) {
            CACHE.put(i, String.valueOf(i));
        }
    }

    @Transactional
    public String queryByIdFromCache(Integer id) {
        return CACHE.getOrDefault(id, "");
    }

}

重新运行测试后,我这次得到了这个分析结果:

可以看出,事务方面对DemoService.queryByIdFromCache 的调用慢了大约 14.5 (10.2/0.708) 倍。

要找出服务方法变慢的根本原因,我建议您设置一个类似的测试并使用 NetBeans Profiler(或类似的工具)对其进行分析。

【讨论】:

  • 非常感谢。我已经设置log4j调试级别来跟踪,发现原因是aop在真正进入方法之前拦截开始事务并在返回后提交事务。因为性能对我们来说非常关键,我决定直接从mysql-connector调用低级api- java.
猜你喜欢
  • 2011-10-11
  • 2021-04-09
  • 2011-03-02
  • 2014-10-15
  • 2023-01-12
  • 2013-11-12
  • 2011-12-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多