Search code examples
javaspringaopaspectjbytecode

Aspect breaking bytecode on specific class


I'm new to AOP, I've created an aspect to trace all methods or classes marked with @Trace annotation. I'm using compile time weaving. (Java 8, Aspectj 1.8, Spring 4)

TraceAspect.java

@Aspect
public class TraceAspect {
    private static Map<String, Integer> threadMap = new HashMap<>();

    @Pointcut("@within(Trace) || @annotation(Trace)")
    void annotated(){}
    
    @Around("annotated() && execution(* *(..))")
    public Object trace(final ProceedingJoinPoint joinPoint) throws Throwable {
        String  threadName = Thread.currentThread().getName();

        String indent = indent(inThread(threadName));

        System.out.println(threadName + " : " + indent + "->  " + joinPoint.getSignature().toString());

        long start = System.nanoTime();
        Object ret = joinPoint.proceed();
        long end = System.nanoTime();

        System.out.println(threadName + " : " + indent + "<-  " + joinPoint.getSignature().toString() + " ended (took " + (end - start) + " nanoseconds)");

        outThread(threadName);
        return ret;
    }

    private String indent(int depth) {
        String result = "";
        for (int index = 0; index < depth; index++) {
            result += "   ";
        }
        return result;
    }

    private int inThread(String threadName) {
        if (threadMap.get(threadName) == null) {
            threadMap.put(threadName, 0);
        }

        int stackDepth = threadMap.get(threadName) + 1;
        threadMap.put(threadName, stackDepth);
        return stackDepth;
    }

    private void outThread(String threadName) {
        int stackDepth = threadMap.get(threadName) - 1;
        threadMap.put(threadName, stackDepth);
    }
}

The CryptsyExchange.java (which is a Spring Bean) when marked with @Trace, classloader throws ClassFormat error on build(..) method while initializing that bean in application context...

CryptsyExchange.java

@Trace
public class CryptsyExchange {
    private static final Logger LOGGER = LoggerFactory.getLogger(CryptsyExchange.class);

    private DataService dataService;
    private Configuration config;

    private Converter converter;
    private Exchange exchange;
    private List<CryptsyAccount> accounts = Collections.synchronizedList(new LinkedList<>());
    private CryptsyAccount defaultAccount;

    public static CryptsyExchange build(String name, DataService dataService, ConfigPathBuilder pathBuilder) {
        condition(notNullOrEmpty(name) && notNull(dataService, pathBuilder));

        CryptsyExchange cryptsyExchange = new CryptsyExchange();
        cryptsyExchange.dataService = dataService;


        // Loading configuration
        final Configuration configuration = Configuration.load(pathBuilder.getExchangeConfigPath(name));
        cryptsyExchange.config = configuration;

        // Retrieve corresponding exchange from datastore
        cryptsyExchange.exchange = dataService.registerExchange(cryptsyExchange.config.getString("exchange"));

        // Get accounts from configuration

        Map<String, Map<String, String>> accounts = configuration.getMap("accounts");

        // Initialize accounts
        accounts.entrySet().stream().forEach((entry) -> {
            String key = entry.getKey();
            Map<String, String> accountMap = entry.getValue();

            // Retrieve corresponding datastore account
            Account account = dataService.registerAccount(cryptsyExchange.exchange, key);

            // Initialize cryptsy specific account
            CryptsyAccount cryptsyAccount = new CryptsyAccount(account, accountMap.get("key"), accountMap.get("secret"));

            cryptsyExchange.accounts.add(cryptsyAccount);

            if (notNull(accountMap.get("isDefault")) && Boolean.valueOf(accountMap.get("isDefault"))) {
                cryptsyExchange.defaultAccount = cryptsyAccount;
            }
        });

        // Initializing Converter
        cryptsyExchange.converter = cryptsyExchange.new Converter();

        // Recover associations from configuration
        Map<String, String> exchangeCurrencyToCurrency = configuration.getMap("exchangeCurrencyToCurrency");
        Set<String> markedForRemoval = new HashSet<>();

        exchangeCurrencyToCurrency.entrySet().stream().forEach((entry) -> {
            String cryptsyCurrencyCode = entry.getKey();
            String currencySymbol = entry.getValue();

            com.jarvis.data.entity.Currency currency = dataService.getCurrency(currencySymbol);
            if (notNull(currency)) {
                cryptsyExchange.converter.associateCurrency(currency, cryptsyCurrencyCode);
            } else {
                LOGGER.debug("associated currency [" + currencySymbol + "] does not exist in database, removing from configuration");
                markedForRemoval.add(cryptsyCurrencyCode);
            }
        });

        // Removing currency associations missing from database
        if (!markedForRemoval.isEmpty()) {
            markedForRemoval.forEach((currency) -> configuration.remove("exchangeCurrencyToCurrency", currency));
        }

        Map<String, String> exchangeMarketToMarket = configuration.getMap("exchangeMarketToMarket");
        markedForRemoval.clear();

        exchangeMarketToMarket.entrySet().stream().forEach((entry) -> {
            String cryptsyMarketId = entry.getKey();
            String marketName = entry.getValue();

            Market market = dataService.getMarket(marketName);
            if (notNull(market)) {
                cryptsyExchange.converter.associateMarket(market, Integer.valueOf(cryptsyMarketId));
            } else {
                LOGGER.debug("associated market [+" + marketName + "] does not exist, removing from configuration");
                markedForRemoval.add(cryptsyMarketId);
            }
        });

        // Removing market associations missing from database
        if (!markedForRemoval.isEmpty()) {
            markedForRemoval.forEach((market) -> configuration.remove("exchangeMarketToMarket", market));
        }

        // Update configuration
        configuration.save();

        return cryptsyExchange;
    }

    // Lot of other code there
}

And of course the stackTrace:

Exception in thread "main" java.lang.ClassFormatError: Illegal local variable table length 288 in method com.jarvis.exchange.cryptsy.CryptsyExchange.build_aroundBody0(Ljava/lang/String;Lcom/jarvis/data/service/DataService;Lcom/jarvis/util/ConfigPathBuilder;Lorg/aspectj/lang/JoinPoint;)Lcom/jarvis/exchange/cryptsy/CryptsyExchange;
    at java.lang.Class.getDeclaredMethods0(Native Method)
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2688)
    at java.lang.Class.getDeclaredMethods(Class.java:1962)
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:467)
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:451)
    at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:512)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:663)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:593)
    at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1396)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:382)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:353)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:82)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:609)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:464)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
    at com.jarvis.Jarvis.<clinit>(Jarvis.java:10)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:259)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:116)

I've tried this on any other class on my project (annotation can be applied on Type or method) and it worked, but exactly with this bean build method I'm facing issues and can't find any workaround. Maybe current support for Java 8 by Aspectj is buggy and it actually corrupts bytecode. Or perhaps there is something wrong I've done there?


Solution

  • Some questions:

    • Do you use full-blown AspectJ or just Spring AOP with @AspectJ syntax?
    • Do you compile with source/target level 1.8 or maybe still 1.7?
    • Does the exact same code work with Java 7 and AspectJ 1.8.0?
    • If not, does it work with Java 7 and AspectJ 1.7.4?
    • Can you please provide a stand-alone minimal code sample reproducing the problem? Maybe even on GitHub with a Maven build?) Many of your referenced classes are invisible to me, so I cannot reproducte it.

    AJ 1.8.0 is brandnew and some of its problems are actually caused by ECJ (Eclipse Java compiler). Some have already been fixed, so you could try a current developer build (select "Last Known Good developer build".

    Update:

    I was able to reproduce the problem with a small code sample, independent of any other classes. The problem is not annotations or simple forEach lambdas, but obviously nested forEach lambdas.

    I filed an AspectJ bug on http://bugs.eclipse.org/bugs/show_bug.cgi?id=435446.

    Update 2:

    The bug ticket also describes how to work around the problem by excluding lambda calls from the pointcut.

    Another workaround I just found is to run the JVM with parameter -noverify.

    Update 3:

    The bugfix is done and available as a development build. It will be part of the upcoming AspectJ 1.8.1.