Prevenir o ‘Relatório do Crystal Report já fechado’

Quando você tem um aplicativo Java que gera documentos PDF com base em um modelo CR, notará que o carregamento do modelo pela primeira vez levará algum tempo. Uma coisa que você pode fazer é carregar o template como um (Spring) @Bean. Logo você notará que após cerca de 15 minutos o documento de relatório expirará e não poderá mais ser usado para gerar PDFs.

O erro que será exibido é: com.crystaldecisions.sdk.occa.report.lib.ReportSDKException: The report document has already been closed.---- Error code:-2147467259 Error code name:failed

A única solução então é parar seu aplicativo e reiniciá-lo novamente, o que é inaceitável, claro. Outra solução é colocar o bean em um determinado escopo. O Spring, entretanto, não tem um escopo definido que se adapte a essa situação. Mas, felizmente, eles podem ser implementados com bastante facilidade.

Aqui está um exemplo de um escopo personalizado, que reinicializa os beans após 10 minutos de inatividade:

package org.springframework.beans.scope;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class TimeoutScope implements Scope {

private static final long TIMEOUT_IN_MINUTES = 10 * 60 * 1000;

private Map<String, Object> objectMap = Collections.synchronizedMap(new HashMap<String, Object>());
private Map<Object, Long> timeoutMap = Collections.synchronizedMap(new HashMap<Object, Long>());

public Object get(String name, ObjectFactory<?> objectFactory) {
if (objectMap.containsKey(name)) {
Object object = objectMap.get(name);
long timeoutValue = timeoutMap.get(object);

if (System.currentTimeMillis() >= timeoutValue) {
objectMap
.remove(name);
timeoutMap
.remove(object);
}
}

if (!objectMap.containsKey(name)) {
Object object = objectFactory.getObject();
objectMap
.put(name, object);
}

Object object = objectMap.get(name);
timeoutMap
.put(object, System.currentTimeMillis() + TIMEOUT_IN_MINUTES);

return object;
}

public Object remove(String name) {
if (objectMap.containsKey(name)) {
Object object = objectMap.get(name);
timeoutMap
.remove(object);
}

return objectMap.remove(name);
}

public void registerDestructionCallback(String name, Runnable callback) {
}

public Object resolveContextualObject(String key) {
return null;
}

public String getConversationId() {
return null;
}
}

Agora você pode adicionar o escopo à sua configuração, por exemplo, fazendo o seguinte (ou escrever um equivalente em XML para ele):

@Bean
public static CustomScopeConfigurer getCustomScopes() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();

Map<String, Object> scopes = new HashMap<>();
scopes
.put("timeout", new TimeoutScope());

customScopeConfigurer
.setScopes(scopes);

return customScopeConfigurer;
}

E a partir desse momento você pode adicionar o @Scope(value = "timeout")ao @Bean que carrega seu modelo.