Máte jistotu, že do session ukládáte pouze serializovatelné objekty?

Jestli ano, tak by mne velmi zajímalo, jak to děláte. My jsme totiž ještě donedávna žádnou jistotu neměli - vše záleželo na poctivosti a důslednosti programátorů. Jenže v Javě není tahle záležitost vůbec jednoduchá a tak vám může díky nějaké referenci hluboko ve stromu objektů uniknout, že to, co ukládáte do session, má vazbu na objekt, který serializovatelný není. Výsledkem je ztráta session při restartech aplikačního serveru nebo zamezení možnosti session replikovat mezi nody clusteru.

My s tímto problémem bojujeme dlouho a průběžně - naše zbraně však byly dosud poměrně neefektivní. V podstatě jsme se spoléhali na chybové hlášení při restartu Tomcatu, který vypisovalo problémové (neserializovatelné) objekty, kvůli kterým nebylo možné obnovit session. Jinými slovy - spoléhali jsme se na náhodu.

Na nedávném hackathonu jsme si však vyrobili nástroj, který nám umožní s tímto problémem bojovat lépe.


Vycházíme z principu fail-fast - tedy pokud se nám povede do session uložit neserializovatelný objekt, chceme o tom vědět co nejdříve, aby se nám jednodušeji hledal údaj a akce, která do session závadný objekt ukládá. Zároveň není možné se spolehnout pouze na monitoring operace setAttribute, protože tou jsme mohli do session uložit pouze referenci na kontejnerový objekt (např. Map), do kterého se teprve později uloží reference na neserializovatelný objekt.

Implementované řešení

V případě, že aplikace běží ve vývojovém režimu, prochází zpracování requestu servletovým filtrem, který po ukončení zpracování requestu projde všechny atributy session a vyzkouší, zda je možné je všechny serializovat. Ukázku takového filtru přikládám níže (vyhodnocení vývojového režimu je už na vás):


public class SerializabilityCheckFilter implements Filter {
    private static final Log log = LogFactory.getLog(SerializabilityCheckFilter.class);
    public void init(FilterConfig filterConfig) throws ServletException {
        //nothing necessary to do
    }
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } finally {
            if (request instanceof HttpServletRequest) {
                HttpServletRequest httpRequest = (HttpServletRequest) request;
                HttpSession session = httpRequest.getSession(false);
                if (session != null) {
                    boolean serializable = true;
                    StringBuilder items = new StringBuilder();
                    Enumeration<String> names = session.getAttributeNames();
                    while (names.hasMoreElements()) {
                        String attrName = names.nextElement();
                        boolean attributeSerializable = serialize(
                                attrName, session.getAttribute(attrName)
                        );
                        if (!attributeSerializable) {
                            if (items.length() > 0) {
                                items.append(", ");
                            }
                            items.append(attrName);
                        }
                        serializable &= attributeSerializable;
                    }
                    if (!serializable) {
                        throw new ObjectNotSerializableException(
                                "These objects stored in session attributes " +
                                    "are not serializable (see detailed log): " +
                                    items
                        );
                    }
                }
            }
        }
    }
    public void destroy() {
        //nothing necessary to do
    }
    /**
     * Serializes object into byteArray
     *
     * @param attrName
     * @param object
     * @return
     */
    private boolean serialize(String attrName, Object object) {
        ByteArrayOutputStream bos = null;
        ObjectOutput out = null;
        try{
            bos = new ByteArrayOutputStream();
            out = new ObjectOutputStream(bos);
            out.writeObject(object);
            return true;
        } catch (IOException ex) {
            String msg = "Failed to serialize attribute: " + attrName;
            log.error(msg, ex);
            return false;
        } finally {
            IOUtils.closeQuietly(bos);
            if (out != null) {
                try { out.close(); } catch (IOException ignored) {}
            }
        }
    }
}

Výhody řešení

… jsou zřejmé:

  • chyby serializace a výkonnostní penalizace se projeví pouze ve vývojovém režimu - v testovacím ani produkčním se již logika neaplikuje
  • díky vyhazované vyjímce je vývojář NUCEN se problémem zabývat - jinak se mu bude obtížně aplikace vyvíjet
  • v textu vyjímky jsou přehledně uvedený názvy atributů, které se nedaří serializovat - takže vývojář přesně ví, kde je problém a i to, kdy problém nastal, protože to muselo být v aktuálním requestu
  • vývojář je veden k udržování malé velikosti session, jinak se mu bude zpracování requestů zpomalovat díky nutnosti celou session po každém requestu serializovat
  • je velká pravděpodobnost, že většina problémů - ne-li všechny se vychytají průběžně při vývoji aplikace

Výše uvedený nástroj používáme teprve týden a už se nám tímto způsobem podařilo vychytat asi 10 problémů, které v naší aplikaci byly aniž bychom o nich věděli. Řešení každého problému trvalo jen pár minut a každý z nás má teď větší jistotu, že se mu nepovede do aplikace zavléci problém podobného charakteru. Máme totiž v záloze automat, který nikdy nespí!