We moved our application from a own small logging component to Log4j2.
The application runs about 60.000 jobs per day at our biggest installation.
We write our own Appender which writes using Hibernate to our DB (see InnovaIntegrationsportalHibernateAppender).
After a runtime of ~36h the JVM crashed with Out of Memory Exception/Error(OOME), analysing the hprof i saw that there is a amount of 763,5MB of the class org.apache.logging.log4j.core.appender.AbstractManager
see screenshot of the Analyse attached (2019-04-12 13_20_45-eclips...)
https://issues.apache.org/jira/browse/LOG4J2-2589
Going deeper i tried to reconstruct the behavior in a test class and profiled its result (see screenshot-1).
Looks like a memory leak to me.
InnovaIntegrationsportalHibernateAppender
package de.itout.innova.log4j.innova_log4j_appender;
import de.itout.innova.ssp.entities.ssp.entities.*;
import de.itout.jpa.util.*;
import java.io.*;
import java.util.*;
import javax.persistence.*;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.appender.*;
import org.apache.logging.log4j.core.config.plugins.*;
import org.apache.logging.log4j.core.layout.*;
/**
*
* #author swendelmann
*/
#Plugin(name = "InnovaIntegrationsportalHibernateAppender", category = "Core", elementType = "appender")
public class InnovaIntegrationsportalHibernateAppender extends AbstractAppender
{
private String schnittstelle;
private String version;
private String laufId;
private EntityManager em;
public InnovaIntegrationsportalHibernateAppender(String name, Filter filter, Layout<? extends Serializable> layout)
{
super(name, filter, layout);
}
public InnovaIntegrationsportalHibernateAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions)
{
super(name, filter, layout, ignoreExceptions);
}
private InnovaIntegrationsportalHibernateAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions, String schnittstelle, String version, String laufId)
{
super(name, filter, layout, ignoreExceptions);
this.schnittstelle = schnittstelle;
this.version = version;
this.laufId = laufId;
}
// Your custom appender needs to declare a factory method
// annotated with `#PluginFactory`. Log4j will parse the configuration
// and call this factory method to construct an appender instance with
// the configured attributes.
#PluginFactory
public static InnovaIntegrationsportalHibernateAppender createAppender(
#PluginAttribute("name") String name,
#PluginElement("Layout") Layout<? extends Serializable> layout,
#PluginElement("Filter") final Filter filter,
#PluginAttribute("schnittstelle") String schnittstelle,
#PluginAttribute("version") String version,
#PluginAttribute("laufId") String laufId
)
{
if (name == null)
{
LOGGER.error("No name provided for InnovaIntegrationsportalHibernateAppender");
return null;
}
if (layout == null)
{
layout = PatternLayout.createDefaultLayout();
}
if (laufId == null || laufId.isEmpty())
{
LOGGER.error("No laufid provided for InnovaIntegrationsportalHibernateAppender");
}
return new InnovaIntegrationsportalHibernateAppender(name, filter, layout, true, schnittstelle, version, laufId);
}
#Override
public void append(LogEvent event)
{
try
{
em = JPAUtil.getEntityManager("SSP");
em.getTransaction().begin();
LauflogPK lauflogPK = new LauflogPK(schnittstelle, version, laufId, getNextLaufLogPos());
Lauflog lauflog = new Lauflog(lauflogPK);
lauflog.setLevel(event.getLevel().name());
Date eventDateTime = new Date(event.getTimeMillis());
lauflog.setLastchangeAenderungsdatum(eventDateTime);
lauflog.setLastchangeAenderungszeit(eventDateTime);
lauflog.setLastchangeBenutzer("WENDELMANN");
lauflog.setLastchangeLogflag('A');
lauflog.setText(event.getMessage().getFormattedMessage());
em.persist(lauflog);
em.getTransaction().commit();
}
catch (Throwable t)
{
em.getTransaction().rollback();
LOGGER.error("Cant commit to Database InnovaIntegrationsportalHibernateAppender ", t);
//TODO Log to file
}
finally
{
try
{
if (em != null)
{
em.close();
}
}
catch (Throwable t)
{
LOGGER.error("Cant close em: ", t);
}
}
}
/**
* SELECT ISNULL(MAX(POS)+1,0) FROM LAUFLOG
* #return
*/
private int getNextLaufLogPos()
{
Integer i = (Integer) em.createQuery("SELECT MAX(l.lauflogPK.pos)+1 FROM Lauflog l WHERE l.lauflogPK.schnittstelle = :schnittstelle AND l.lauflogPK.version = :version AND l.lauflogPK.lauf = :lauf ")
.setParameter("schnittstelle", this.schnittstelle)
.setParameter("version", this.version)
.setParameter("lauf", this.laufId)
.getSingleResult();
if (i == null)
{
return 0;
}
else
{
return i;
}
}
}
package de.itout.innova.log4j;
import de.itout.innova.log4j.innova_log4j_appender.*;
import de.itout.jpa.util.*;
import java.io.*;
import java.nio.charset.*;
import org.apache.logging.log4j.*;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.appender.*;
import org.apache.logging.log4j.core.config.*;
import org.apache.logging.log4j.core.layout.*;
import org.junit.*;
/**
*
* #author swendelmann
*/
public class ProfilingTest
{
QEntityManager em;
private org.apache.logging.log4j.Logger logg = LogManager.getLogger();
// Potential memory leak
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
public ProfilingTest()
{
}
#Before
public void setUp()
{
em = JPAUtil.getEntityManager("SSP");
}
#After
public void tearDown()
{
em.close();
}
#Test
public void testProfiling()
{
logg.trace("Start the Main Method");
int runs = 1000;
int logs = 10;
String allSicherungsVerzeichnis = "target/sicherungsverzeichnis/";
logg.debug("Start Test mit " + runs + " durchläufen");
for (int i = 0; i < runs; i++)
{
String laufid = "LD0" + i;
File laufLogFile = new File(allSicherungsVerzeichnis + laufid + "_full.log");
Configuration config = ctx.getConfiguration();
// Full File Logger
Layout layout = PatternLayout.newBuilder()
.withConfiguration(config)
.withPattern(PatternLayout.SIMPLE_CONVERSION_PATTERN)
.withCharset(Charset.forName("UTF-8"))
.build();
Appender fullAppender = FileAppender.newBuilder()
.setConfiguration(config)
.setName(laufid + "FULL")
.withFileName(laufLogFile.getAbsolutePath())
.withAppend(true)
.withImmediateFlush(true)
.setIgnoreExceptions(false)
.setLayout(layout)
.build();
fullAppender.start();
config.addAppender(fullAppender);
// Hibernate Logger
Appender appender = InnovaIntegrationsportalHibernateAppender.createAppender(laufid, null, null, "LISA_4711", "LI0001", laufid);
appender.start();
AppenderRef ref = AppenderRef.createAppenderRef(laufid, Level.ALL, null); // HIER LOGLEVEL DB EINSTELLEN
AppenderRef refFull = AppenderRef.createAppenderRef(laufid + "FULL", Level.ALL, null); // HIER LOGLEVEL Datei einstellen
AppenderRef[] refs = new AppenderRef[]
{
ref, refFull
};
LoggerConfig loggerConfig = LoggerConfig
.createLogger(false, Level.ALL, laufid, "true", refs, null, config, null);
loggerConfig.addAppender(appender, Level.ALL, null); // HIER LOGLEVEL ebenfalls einstellen
loggerConfig.addAppender(fullAppender, Level.ALL, null); // HIER LOGLEVEL Datei einstellen
config.addLogger(laufid, loggerConfig);
ctx.updateLoggers();
org.apache.logging.log4j.Logger log = LogManager.getLogger(laufid);
for (int j = 0; j < logs; j++)
{
log.info("Ich bin nur eine Info und soll nur in das FullFile logging!");
log.warn("Ich bin ein böser warning und soll in das FullFile und in das Innova Integrationsportal Hibernate Logging");
}
appender.stop();
fullAppender.stop();
config.removeLogger(laufid);
config.removeLogger(laufid + "FULL");
ctx.updateLoggers();
}
logg.trace("Fertig");
}
}
Update 02.05.2019
I moved to the JDBC logger and changed the DB.
The first test was about fine, but when i programatically add a fileappender + logger with ref to the fileappender & jdbc appender from xml config, i got empty files and no db entries.
I made up a test project on github see: https://github.com/stefanwendelmann/JavaLogging
I am looking at :
https://github.com/stefanwendelmann/JavaLogging/blob/master/src/test/java/LoggerTest.java
I think this is what is happening:
private static final LoggerContext ctx ...
Configuration config = ctx.getConfiguration();
config.addAppender(fullAppender);
and you never remove it, so it keeps adding in the static field. You can debug and see what is inside Log4j2. I haven't debugged yet.
Memory leaks are coming through static fields and through classloaders usually.
Best option is to move away from logging with jdbc. It is designed to work in another way and not to dynamically add/remove appenders, because you have to do all the zookeeping of objects. It may be easier for you to just make a facade pattern and hide the complexity there and only expose couple of methods for write in DB.
Probably, Hibernate keeps objects in its cache. Try evict them immediately after commit.
Related
I have the following legacy class which used PropertyConfigurator under Log4J 1.x
There's no equivalent under Log4J 2.x but I am trying to preserve the functionality at method level.
The existing answers on Stack Overflow are for locating the configuration file, but the legacy code I have in loadConfiguration is setting properties inside the configuration file about where the actual log file will be output.
I wish to know what the equivalent would be under Log4J2. It's not clear to me how the
loadConfiguration method will change due to not having a PropertyConfigurator under log4j 2.x.
Thanks in advance.
Existing code:
public class BasicLogConfigurator {
public static final String LOG_LEVEL_KEY = "log.level.override";
private static final Level DEFAULT_LOG_LEVEL = Level.ERROR;
private static final String LOG4J_PROPS_LOCATION = "/log4j.properties";
private static final String LOG_FILE_PATTERN = "CustomLog_%s.log";
private static final Date TIMESTAMP = new Date();
private static final String METADATA_FOLDER = ".metadata";
public static void loadDefaultConfiguration() {
// 1. Grab the Log file location
File logLocation = getLogFileLocationInMetadataDirectory();
// 2. Grab default Log level
Level logLevel = DEFAULT_LOG_LEVEL;
loadConfiguration(logLocation, logLevel);
}
public static void loadConfiguration(File logLocation, Level logLevel) {
Properties properties = getConfigurationProperties();
// 1. Setup the Log file location
properties.setProperty("log4j.appender.FILE.file", logLocation.getAbsolutePath());
// 2. Setup the default Log level
properties.setProperty("log4j.rootLogger", String.format("%s, CONSOLE, FILE", sanitizeLogLevel(logLevel)));
LogManager.resetConfiguration();
PropertyConfigurator.configure(properties);
}
public static File getLogFileLocationInMetadataDirectory() {
String workspaceDirectory = getWorkspaceDirectory();
File metadataDirectory = new File(workspaceDirectory, METADATA_FOLDER);
File logLocation = getLogFileLocation(metadataDirectory);
return logLocation;
}
public static File getLogFileLocation(File directory) {
return new File(directory, String.format(LOG_FILE_PATTERN, formatTimeStamp(TIMESTAMP)));
}
private BasicLogConfigurator() {
// prevent instantiation
}
private static Level sanitizeLogLevel(Level selectedLevel) {
String overridingLevel = System.getProperty(LOG_LEVEL_KEY);
if (overridingLevel != null) {
return Level.toLevel(overridingLevel, Level.DEBUG);
}
if (selectedLevel != null) {
return selectedLevel;
}
return DEFAULT_LOG_LEVEL;
}
public static String getWorkspaceDirectory() {
return ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString();
}
private static String formatTimeStamp(Date date) {
return new SimpleDateFormat("yyyyMMdd").format(date).toString();
}
public static Properties getConfigurationProperties() {
try {
Properties properties = new Properties();
properties.load(BasicLogConfigurator.class.getResourceAsStream(LOG4J_PROPS_LOCATION));
return properties;
} catch (IOException e) {
throw new RuntimeException(
String.format("Error while loading {%s}", LOG4J_PROPS_LOCATION), e);
}
}
}
So how is this done for Log4J2 given the inputs of the loadConfiguration method? And does getConfigurationProperties need to change to work with Log4J2?
Trying to use java.util.logging and failing.
In an attempt to make use of https://stackoverflow.com/a/8249319/3322533 :
handlers = mypackage.logging.RequestFileHandler, mypackage.logging.MainFileHandler
config =
mainLogger.handlers = mypackage.logging.MainFileHandler
requestLogger.handlers = mypackage.logging.RequestFileHandler
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.filter =
java.util.logging.ConsoleHandler.formatter = mypackage.logging.VerySimpleFormatter
java.util.logging.ConsoleHandler.encoding =
mypackage.RequestFileHandler.level = SEVERE
mypackage.RequestFileHandler.filter =
mypackage.RequestFileHandler.formatter = mypackage.logging.VerySimpleFormatter
mypackage.RequestFileHandler.encoding =
mypackage.RequestFileHandler.limit =
mypackage.RequestFileHandler.count =
mypackage.RequestFileHandler.append = false
mypackage.RequestFileHandler.pattern = REQUESTS.%u.%g.log
mypackage.MainFileHandler.level = INFO
mypackage.MainFileHandler.filter =
mypackage.MainFileHandler.formatter = mypackage.logging.VerySimpleFormatter
mypackage.MainFileHandler.encoding =
mypackage.MainFileHandler.limit =
mypackage.MainFileHandler.count =
mypackage.MainFileHandler.append = false
mypackage.MainFileHandler.pattern = MAIN.%u.%g.log
where
public class MainFileHandler extends FileHandler {
public MainFileHandler() throws IOException, SecurityException {
super();
}
}
and
public class RequestFileHandler extends FileHandler {
public RequestFileHandler() throws IOException, SecurityException {
super();
}
}
Intention: provide two loggers accessible through
Logger.getLogger("mainLogger");
or
Logger.getLogger("requestLogger");
respectively, one that will write (exclusively) to MAIN[...].log and the other to REQUESTS[...].log
(No limits on the amount of messages that can be logged to either file and if necessary, use logging level to filter out unwanted msgs to either.)
However, neither file is created when I (for example)
public static final Logger log = Logger.getLogger("mainLogger");
and then
public void configureLogger(){
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream config = classLoader.getResourceAsStream("logging.properties");
LogManager.getLogManager().readConfiguration(config);
}catch(Exception ex){
throw new RuntimeException("logging properties failed");
}
}
before I
log.info("Hello World!")
I know the properties are loaded because when I include java.util.logging.ConsoleHandler in the handlers = ... list and use the global logger, instead, the formatter is applied for the console output.
So ... I guess my attempt at setting up the file loggers is faulty. How do I get this working?
EDIT
So I removed the [...].pattern = [...] lines and instead hardcoded the file names:
public class MainFileHandler extends FileHandler implements FileHandlerProperties {
public MainFileHandler() throws IOException, SecurityException {
super("MAIN_" + new SimpleDateFormat(TIME_PATTERN).format(new Date()) + ".log");
}
}
and
public class RequestFileHandler extends FileHandler implements FileHandlerProperties {
public RequestFileHandler() throws IOException, SecurityException {
super("REQUESTS_" + new SimpleDateFormat(TIME_PATTERN).format(new Date()) + ".log");
}
}
where
public interface FileHandlerProperties {
static final String TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
}
Both files now get created BUT they both contain exactly the same (despite their different level settings and loggers) AND what they contain is in xml:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2016-10-10T18:49:23</date>
<millis>1476118163654</millis>
<sequence>0</sequence>
<logger>mainLogger</logger>
<level>INFO</level>
<class>mypackage.main.Main</class>
<method><init></method>
<thread>1</thread>
<message>Hello World</message>
</record>
</log>
Please help ...
The problem is that the first call to Logger.getLogger during class loading reads the log configuration and your configureLogger method fails due to JDK-8033661: readConfiguration does not cleanly reinitialize the logging system.
To workaround this you have to ensure that configureLogger runs before the first call to Logger.getLogger.
public class BootMain {
static {
configureLogger();
mainLogger = Logger.getLogger("mainLogger");
requestLogger = Logger.getLogger("requestLogger");
}
private static final Logger mainLogger;
private static final Logger requestLogger;
public static void main(String[] args) throws IOException {
mainLogger.log(Level.SEVERE, "Test from main.");
requestLogger.log(Level.SEVERE, "Test from request.");
System.out.println(new File(".").getCanonicalPath());
}
private static void configureLogger() {
try {
InputStream config = config();
LogManager.getLogManager().readConfiguration(config);
} catch (Exception ex) {
throw new RuntimeException("logging properties failed");
}
}
private static String prefix() {
return "mypackage.logging";
}
private static InputStream config() throws IOException {
String p = prefix();
Properties props = new Properties();
props.put("mainLogger.handlers", p + ".MainFileHandler");
props.put("requestLogger.handlers", p + ".RequestFileHandler");
props.put(p + ".RequestFileHandler.level", "SEVERE");
props.put(p + ".MainFileHandler.level", "INFO");
props.put(p + ".RequestFileHandler.pattern", "REQUESTS.%u.%g.log");
props.put(p + ".MainFileHandler.pattern", "MAIN.%u.%g.log");
ByteArrayOutputStream out = new ByteArrayOutputStream();
props.store(out, "");
return new ByteArrayInputStream(out.toByteArray());
}
}
Also make sure you are not using a really old version of JDK or you can run into JDK-5089480: java.util.logging.FileHandler uses hardcoded classname when reading properties.
Otherwise you can use the LogManager config option to manually setup your configuration.
With Spring 4 and Hibernate 4, I was able to use Reflection to get the Hibernate Configuration object from the current environment, using this code:
#Autowired LocalContainerEntityManagerFactoryBean lcemfb;
EntityManagerFactoryImpl emf = (EntityManagerFactoryImpl) lcemfb.getNativeEntityManagerFactory();
SessionFactoryImpl sf = emf.getSessionFactory();
SessionFactoryServiceRegistryImpl serviceRegistry = (SessionFactoryServiceRegistryImpl) sf.getServiceRegistry();
Configuration cfg = null;
try {
Field field = SessionFactoryServiceRegistryImpl.class.getDeclaredField("configuration");
field.setAccessible(true);
cfg = (Configuration) field.get(serviceRegistry);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
SchemaUpdate update = new SchemaUpdate(serviceRegistry, cfg);
With Hibernate 5, I must use some MetadataImplementor, which doesn't seems to be available from any of those objects. I also tried to use MetadataSources with the serviceRegistry. But it did say that it's the wrong kind of ServiceRegistry.
Is there any other way to get this working?
Basic idea for this problem is:
implementation of org.hibernate.integrator.spi.Integrator which stores required data to some holder. Register implementation as a service and use it where you need.
Work example you can find here https://github.com/valery-barysok/spring4-hibernate5-stackoverflow-34612019
create org.hibernate.integrator.api.integrator.Integrator class
import hello.HibernateInfoHolder;
import org.hibernate.boot.Metadata;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
public class Integrator implements org.hibernate.integrator.spi.Integrator {
#Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
HibernateInfoHolder.setMetadata(metadata);
HibernateInfoHolder.setSessionFactory(sessionFactory);
HibernateInfoHolder.setServiceRegistry(serviceRegistry);
}
#Override
public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
}
}
create META-INF/services/org.hibernate.integrator.spi.Integrator file
org.hibernate.integrator.api.integrator.Integrator
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.tool.hbm2ddl.SchemaExport;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void run(String... args) throws Exception {
new SchemaExport((MetadataImplementor) HibernateInfoHolder.getMetadata()).create(true, true);
new SchemaUpdate(HibernateInfoHolder.getServiceRegistry(), (MetadataImplementor) HibernateInfoHolder.getMetadata()).execute(true, true);
}
}
I would like to add up on Aviad's answer to make it complete as per OP's request.
The internals:
In order to get an instance of MetadataImplementor, the workaround is to register an instance of SessionFactoryBuilderFactory through Java's ServiceLoader facility. This registered service's getSessionFactoryBuilder method is then invoked by MetadataImplementor with an instance of itself, when hibernate is bootstrapped. The code references are below:
Service Loading
Invocation of getSessionFactoryBuilder
So, ultimately to get an instance of MetadataImplementor, you have to implement SessionFactoryBuilderFactory and register so ServiceLoader can recognize this service:
An implementation of SessionFactoryBuilderFactory:
public class MetadataProvider implements SessionFactoryBuilderFactory {
private static MetadataImplementor metadata;
#Override
public SessionFactoryBuilder getSessionFactoryBuilder(MetadataImplementor metadata, SessionFactoryBuilderImplementor defaultBuilder) {
this.metadata = metadata;
return defaultBuilder; //Just return the one provided in the argument itself. All we care about is the metadata :)
}
public static MetadataImplementor getMetadata() {
return metadata;
}
}
In order to register the above, create simple text file in the following path(assuming it's a maven project, ultimately we need the 'META-INF' folder to be available in the classpath):
src/main/resources/META-INF/services/org.hibernate.boot.spi.SessionFactoryBuilderFactory
And the content of the text file should be a single line(can even be multiple lines if you need to register multiple instances) stating the fully qualified class path of your implementation of SessionFactoryBuilderFactory. For example, for the above class, if your package name is 'com.yourcompany.prj', the following should be the content of the file.
com.yourcompany.prj.MetadataProvider
And that's it, if you run your application, spring app or standalone hibernate, you will have an instance of MetadataImplementor available through a static method once hibernate is bootstraped.
Update 1:
There is no way it can be injected via Spring. I digged into Hibernate's source code and the metadata object is not stored anywhere in SessionFactory(which is what we get from Spring). So, it's not possible to inject it. But there are two options if you want it in Spring's way:
Extend existing classes and customize all the way from
LocalSessionFactoryBean -> MetadataSources -> MetadataBuilder
LocalSessionFactoryBean is what you configure in Spring and it has an object of MetadataSources. MetadataSources creates MetadataBuilder which in turn creates MetadataImplementor. All the above operations don't store anything, they just create object on the fly and return. If you want to have an instance of MetaData, you should extend and modify the above classes so that they store a local copy of respective objects before they return. That way you can have a reference to MetadataImplementor. But I wouldn't really recommend this unless it's really needed, because the APIs might change over time.
On the other hand, if you don't mind building a MetaDataImplemetor from SessionFactory, the following code will help you:
EntityManagerFactoryImpl emf=(EntityManagerFactoryImpl)lcemfb.getNativeEntityManagerFactory();
SessionFactoryImpl sf=emf.getSessionFactory();
StandardServiceRegistry serviceRegistry = sf.getSessionFactoryOptions().getServiceRegistry();
MetadataSources metadataSources = new MetadataSources(new BootstrapServiceRegistryBuilder().build());
Metadata metadata = metadataSources.buildMetadata(serviceRegistry);
SchemaUpdate update=new SchemaUpdate(serviceRegistry,metadata); //To create SchemaUpdate
// You can either create SchemaExport from the above details, or you can get the existing one as follows:
try {
Field field = SessionFactoryImpl.class.getDeclaredField("schemaExport");
field.setAccessible(true);
SchemaExport schemaExport = (SchemaExport) field.get(serviceRegistry);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
Take a look on this one:
public class EntityMetaData implements SessionFactoryBuilderFactory {
private static final ThreadLocal<MetadataImplementor> meta = new ThreadLocal<>();
#Override
public SessionFactoryBuilder getSessionFactoryBuilder(MetadataImplementor metadata, SessionFactoryBuilderImplementor defaultBuilder) {
meta.set(metadata);
return defaultBuilder;
}
public static MetadataImplementor getMeta() {
return meta.get();
}
}
Take a look on This Thread which seems to answer your needs
Well, my go to on this:
public class SchemaTranslator {
public static void main(String[] args) throws Exception {
new SchemaTranslator().run();
}
private void run() throws Exception {
String packageName[] = { "model"};
generate(packageName);
}
private List<Class<?>> getClasses(String packageName) throws Exception {
File directory = null;
try {
ClassLoader cld = getClassLoader();
URL resource = getResource(packageName, cld);
directory = new File(resource.getFile());
} catch (NullPointerException ex) {
throw new ClassNotFoundException(packageName + " (" + directory + ") does not appear to be a valid package");
}
return collectClasses(packageName, directory);
}
private ClassLoader getClassLoader() throws ClassNotFoundException {
ClassLoader cld = Thread.currentThread().getContextClassLoader();
if (cld == null) {
throw new ClassNotFoundException("Can't get class loader.");
}
return cld;
}
private URL getResource(String packageName, ClassLoader cld) throws ClassNotFoundException {
String path = packageName.replace('.', '/');
URL resource = cld.getResource(path);
if (resource == null) {
throw new ClassNotFoundException("No resource for " + path);
}
return resource;
}
private List<Class<?>> collectClasses(String packageName, File directory) throws ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
if (directory.exists()) {
String[] files = directory.list();
for (String file : files) {
if (file.endsWith(".class")) {
// removes the .class extension
classes.add(Class.forName(packageName + '.' + file.substring(0, file.length() - 6)));
}
}
} else {
throw new ClassNotFoundException(packageName + " is not a valid package");
}
return classes;
}
private void generate(String[] packagesName) throws Exception {
Map<String, String> settings = new HashMap<String, String>();
settings.put("hibernate.hbm2ddl.auto", "drop-create");
settings.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL94Dialect");
MetadataSources metadata = new MetadataSources(
new StandardServiceRegistryBuilder()
.applySettings(settings)
.build());
for (String packageName : packagesName) {
System.out.println("packageName: " + packageName);
for (Class<?> clazz : getClasses(packageName)) {
System.out.println("Class: " + clazz);
metadata.addAnnotatedClass(clazz);
}
}
SchemaExport export = new SchemaExport(
(MetadataImplementor) metadata.buildMetadata()
);
export.setDelimiter(";");
export.setOutputFile("db-schema.sql");
export.setFormat(true);
export.execute(true, false, false, false);
}
}
I have a problem with an old project: The log file doesn't append in the console view of Eclipse. Instead of declaring the properties of log4j uses in log4j.xml or log4j.properties, the logger is defined in java:
import org.apache.log4j.FileAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
public class GCLogger {
private static GCLogger _self;
private Logger logger;
private static final String NAME_FILE = "fichierLog.log";
public static GCLogger getInstance() {
if (_self == null || _self.logger == null) {
GCLogger logger = new GCLogger();
logger.initLogger();
_self = logger;
}
return _self;
}
public void error(String msg, Exception e) {
if (logger != null) {
logger.error(msg, e);
}
}
public void debug(String msg) {
if (logger != null) {
logger.debug(msg);
}
}
private void initLogger() {
logger = Logger.getRootLogger();
FileAppender fa = new FileAppender();
PatternLayout monLayout = new PatternLayout("%d{DATE} - %5p %c{1} - %m%n");
logger.removeAllAppenders();
try {
File repLogs = new File(GCConstants.GC_REPERTOIRE_LOGS);
if (!repLogs.exists()) {
repLogs.mkdir();
}
fa = new FileAppender(monLayout, GCConstants.GC_REPERTOIRE_LOGS + File.separator + NAME_FILE, true);
fa.activateOptions();
fa.setImmediateFlush(true);
logger.addAppender(fa);
logger.setLevel(Level.ALL);
} catch(Exception e) {
logger = null;
}
}
}
Is there a simple way to show fichierLog.log in the console view (using eclipse configuration)?
If not, what is the minimal change to make it works?
Minimum change - add a console appender:
ConsoleAppender console = new ConsoleAppender(monLayout); // re-use the layout
logger.addAppender(console);
<opinion>
However, if you're making other changes to the codebase, I would strongly recommend changing this to use external configuration - it'll make your life much easier in the long (and probably short) term.</opinion>
In a Java servlet container (preferably Tomcat, but if this can be done in a different container then say so) I desire something which is theoretically possible. My question here is whether tools exist to support it, and if so what tools (or what names I should research further).
Here is my problem: in one servlet container I want to run a large number of different WAR files. They share some large common libraries (such as Spring). At first blush, I have two unacceptable alternatives:
Include the large library (Spring, for example) in each WAR file. This is unacceptable because it will load a large number of copies of Spring, exhausting the memory on the server.
Place the large library in the container classpath. Now all of the WAR files share one instance of the library (good). But this is unacceptable because I cannot upgrade the Spring version without upgrading ALL of the WAR files at once, and such a large change is difficult verging on impossible.
In theory, though, there is an alternative which could work:
Put each version of the large library into the container-level classpath. Do some container level magic so that each WAR file declares which version it wishes to use and it will find that on its classpath.
The "magic" must be done at the container level (I think) because this can only be achieved by loading each version of the library with a different classloader, then adjusting what classloaders are visible to each WAR file.
So, have you ever heard of doing this? If so, how? Or tell me what it is called so I can research further.
Regarding Tomcat, for the 7th version you can use VirtualWebappLocader like so
<Context>
<Loader className="org.apache.catalina.loader.VirtualWebappLoader"
virtualClasspath="/usr/shared/lib/spring-3/*.jar,/usr/shared/classes" />
</Context>
For the 8th version Pre- & Post- Resources should be used instead
<Context>
<Resources>
<PostResources className="org.apache.catalina.webresources.DirResourceSet"
base="/usr/shared/lib/spring-3" webAppMount="/WEB-INF/lib" />
<PostResources className="org.apache.catalina.webresources.DirResourceSet"
base="/usr/shared/classes" webAppMount="/WEB-INF/classes" />
</Resources>
</Context>
Don't forget to put the corresponding context.xml into the META-INF of your webapp.
For the jetty as well as other containers the same technique may be used.
The only difference is in how to specify extra classpath elements for the webapp.
UPDATE
The samples above does not share the loaded classes, but the idea is the same - use custom classloader. Here is just the pretty ugly sample that also tries to prevent classloader leaks during undeployment.
SharedWebappLoader
package com.foo.bar;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.loader.WebappLoader;
public class SharedWebappLoader extends WebappLoader {
private String pathID;
private String pathConfig;
static final ThreadLocal<ClassLoaderFactory> classLoaderFactory = new ThreadLocal<>();
public SharedWebappLoader() {
this(null);
}
public SharedWebappLoader(ClassLoader parent) {
super(parent);
setLoaderClass(SharedWebappClassLoader.class.getName());
}
public String getPathID() {
return pathID;
}
public void setPathID(String pathID) {
this.pathID = pathID;
}
public String getPathConfig() {
return pathConfig;
}
public void setPathConfig(String pathConfig) {
this.pathConfig = pathConfig;
}
#Override
protected void startInternal() throws LifecycleException {
classLoaderFactory.set(new ClassLoaderFactory(pathConfig, pathID));
try {
super.startInternal();
} finally {
classLoaderFactory.remove();
}
}
}
SharedWebappClassLoader
package com.foo.bar;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.loader.ResourceEntry;
import org.apache.catalina.loader.WebappClassLoader;
import java.net.URL;
public class SharedWebappClassLoader extends WebappClassLoader {
public SharedWebappClassLoader(ClassLoader parent) {
super(SharedWebappLoader.classLoaderFactory.get().create(parent));
}
#Override
protected ResourceEntry findResourceInternal(String name, String path) {
ResourceEntry entry = super.findResourceInternal(name, path);
if(entry == null) {
URL url = parent.getResource(name);
if (url == null) {
return null;
}
entry = new ResourceEntry();
entry.source = url;
entry.codeBase = entry.source;
}
return entry;
}
#Override
public void stop() throws LifecycleException {
ClassLoaderFactory.removeLoader(parent);
}
}
ClassLoaderFactory
package com.foo.bar;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class ClassLoaderFactory {
private static final class ConfigKey {
private final String pathConfig;
private final String pathID;
private ConfigKey(String pathConfig, String pathID) {
this.pathConfig = pathConfig;
this.pathID = pathID;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConfigKey configKey = (ConfigKey) o;
if (pathConfig != null ? !pathConfig.equals(configKey.pathConfig) : configKey.pathConfig != null)
return false;
if (pathID != null ? !pathID.equals(configKey.pathID) : configKey.pathID != null) return false;
return true;
}
#Override
public int hashCode() {
int result = pathConfig != null ? pathConfig.hashCode() : 0;
result = 31 * result + (pathID != null ? pathID.hashCode() : 0);
return result;
}
}
private static final Map<ConfigKey, ClassLoader> loaders = new HashMap<>();
private static final Map<ClassLoader, ConfigKey> revLoaders = new HashMap<>();
private static final Map<ClassLoader, Integer> usages = new HashMap<>();
private final ConfigKey key;
public ClassLoaderFactory(String pathConfig, String pathID) {
this.key = new ConfigKey(pathConfig, pathID);
}
public ClassLoader create(ClassLoader parent) {
synchronized (loaders) {
ClassLoader loader = loaders.get(key);
if(loader != null) {
Integer usageCount = usages.get(loader);
usages.put(loader, ++usageCount);
return loader;
}
Properties props = new Properties();
try (InputStream is = new BufferedInputStream(new FileInputStream(key.pathConfig))) {
props.load(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
String libsStr = props.getProperty(key.pathID);
String[] libs = libsStr.split(File.pathSeparator);
URL[] urls = new URL[libs.length];
try {
for(int i = 0, len = libs.length; i < len; i++) {
urls[i] = new URL(libs[i]);
}
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
loader = new URLClassLoader(urls, parent);
loaders.put(key, loader);
revLoaders.put(loader, key);
usages.put(loader, 1);
return loader;
}
}
public static void removeLoader(ClassLoader parent) {
synchronized (loaders) {
Integer val = usages.get(parent);
if(val > 1) {
usages.put(parent, --val);
} else {
usages.remove(parent);
ConfigKey key = revLoaders.remove(parent);
loaders.remove(key);
}
}
}
}
context.xml of the first app
<Context>
<Loader className="com.foo.bar.SharedWebappLoader"
pathConfig="${catalina.base}/conf/shared.properties"
pathID="commons_2_1"/>
</Context>
context.xml of the second app
<Context>
<Loader className="com.foo.bar.SharedWebappLoader"
pathConfig="${catalina.base}/conf/shared.properties"
pathID="commons_2_6"/>
</Context>
$TOMCAT_HOME/conf/shared.properties
commons_2_1=file:/home/xxx/.m2/repository/commons-lang/commons-lang/2.1/commons-lang-2.1.jar
commons_2_6=file:/home/xxx/.m2/repository/commons-lang/commons-lang/2.6/commons-lang-2.6.jar
I was able to implement this for Tomcat (Tested on Tomcat 7.0.52). My solution involves implementing custom version of WebAppLoader which extends standard Tomcat's WebAppLoader. Thanks to this solution you can pass custom classloader to load classes for each of web application.
To utilize this new loader you need to declare it for each application (either in Context.xml file placed in each war or in Tomcat's server.xml file). This loader takes an extra custom parameter webappName which is later passed to LibrariesStorage class to determine which libraries should be used by which application.
<Context path="/pl-app" >
<Loader className="web.DynamicWebappLoader" webappName="pl-app"/>
</Context>
<Context path="/my-webapp" >
<Loader className="web.DynamicWebappLoader" webappName="myApplication2"/>
</Context>
Once this is defined you need to install this DynamicWebappLoader to Tomcat. To do this copy all copiled classes to lib directory of Tomcat (so you should have following files [tomcat dir]/lib/web/DynamicWebappLoader.class, [tomcat dir]/lib/web/LibrariesStorage.class, [tomcat dir]/lib/web/LibraryAndVersion.class, [tomcat dir]/lib/web/WebAppAwareClassLoader.class).
You need also to download xbean-classloader-4.0.jar and place it in Tomcat's lib dir (so you should have [tomcat dir]/lib/xbean-classloader-4.0.jar. NOTE:xbean-classloader provides special implementation of classloader (org.apache.xbean.classloader.JarFileClassLoader) which allowes to load needed jars at runtime.
Main trick is made in LibraryStorgeClass (full implementation is at the end). It stores a mapping between each application (defined by webappName) and libraries which this application is allowed to load. In current implementation this is hardcoded, but this can be rewritten to dynamically generate list of libs needed by each application. Each library has its own instance of JarFileClassLoader which ensures that each library is only loaded one time (the mapping between library and its classloader is stored in static field "libraryToClassLoader", so this mapping is the same for every web application because of static nature of the field)
class LibrariesStorage {
private static final String JARS_DIR = "D:/temp/idea_temp_proj2_/some_jars";
private static Map<LibraryAndVersion, JarFileClassLoader> libraryToClassLoader = new HashMap<>();
private static Map<String, List<LibraryAndVersion>> webappLibraries = new HashMap<>();
static {
try {
addLibrary("commons-lang3", "3.3.2", "commons-lang3-3.3.2.jar"); // instead of this lines add some intelligent directory scanner which will detect all jars and their versions in JAR_DIR
addLibrary("commons-lang3", "3.3.1", "commons-lang3-3.3.1.jar");
addLibrary("commons-lang3", "3.3.0", "commons-lang3-3.3.0.jar");
mapApplicationToLibrary("pl-app", "commons-lang3", "3.3.2"); // instead of manually mapping application to library version, some more intelligent code should be here (for example you can scann Web-Inf/lib of each application and detect needed jars
mapApplicationToLibrary("myApplication2", "commons-lang3", "3.3.0");
(...)
}
In above example, suppose that in directory with all the jars (defined here by JARS_DIR) we have only a commons-lang3-3.3.2.jar file. This would mean that application identified by "pl-app" name (the name comes from webappName attribute in tag in Context.xml as mentioned above) will be able to load classes from commons-lang jar. Application identified by "myApplication2" will get ClassNotFoundException at this point because it has access only to commons-lang3-3.3.0.jar, but this file is not present in JARS_DIR directory.
Full implementation here:
package web;
import org.apache.catalina.loader.WebappLoader;
import org.apache.xbean.classloader.JarFileClassLoader;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DynamicWebappLoader extends WebappLoader {
private String webappName;
private WebAppAwareClassLoader webAppAwareClassLoader;
public static final ThreadLocal lastCreatedClassLoader = new ThreadLocal();
public DynamicWebappLoader() {
super(new WebAppAwareClassLoader(Thread.currentThread().getContextClassLoader()));
webAppAwareClassLoader = (WebAppAwareClassLoader) lastCreatedClassLoader.get(); // unfortunately I did not find better solution to access new instance of WebAppAwareClassLoader created in previous line so I passed it via thread local
lastCreatedClassLoader.remove();
}
// (this method is called by Tomcat because of Loader attribute in Context.xml - <Context> <Loader className="..." webappName="myApplication2"/> )
public void setWebappName(String name) {
System.out.println("Setting webapp name: " + name);
this.webappName = name;
webAppAwareClassLoader.setWebAppName(name); // pass web app name to ClassLoader
}
}
class WebAppAwareClassLoader extends ClassLoader {
private String webAppName;
public WebAppAwareClassLoader(ClassLoader parent) {
super(parent);
DynamicWebappLoader.lastCreatedClassLoader.set(this); // store newly created instance in ThreadLocal .. did not find better way to access the reference later in code
}
#Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
System.out.println("Load class: " + className + " for webapp: " + webAppName);
try {
return LibrariesStorage.loadClassForWebapp(webAppName, className);
} catch (ClassNotFoundException e) {
System.out.println("JarFileClassLoader did not find class: " + className + " " + e.getMessage());
return super.loadClass(className);
}
}
public void setWebAppName(String webAppName) {
this.webAppName = webAppName;
}
}
class LibrariesStorage {
private static final String JARS_DIR = "D:/temp/idea_temp_proj2_/some_jars";
private static Map<LibraryAndVersion, JarFileClassLoader> libraryToClassLoader = new HashMap<>();
private static Map<String, List<LibraryAndVersion>> webappLibraries = new HashMap<>();
static {
try {
addLibrary("commons-lang3", "3.3.2", "commons-lang3-3.3.2.jar"); // instead of this lines add some intelligent directory scanner which will detect all jars and their versions in JAR_DIR
addLibrary("commons-lang3", "3.3.1", "commons-lang3-3.3.1.jar");
addLibrary("commons-lang3", "3.3.0", "commons-lang3-3.3.0.jar");
mapApplicationToLibrary("pl-app", "commons-lang3", "3.3.2"); // instead of manually mapping application to library version, some more intelligent code should be here (for example you can scann Web-Inf/lib of each application and detect needed jars
mapApplicationToLibrary("myApplication2", "commons-lang3", "3.3.0");
} catch (MalformedURLException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
private static void mapApplicationToLibrary(String applicationName, String libraryName, String libraryVersion) {
LibraryAndVersion libraryAndVersion = new LibraryAndVersion(libraryName, libraryVersion);
if (!webappLibraries.containsKey(applicationName)) {
webappLibraries.put(applicationName, new ArrayList<LibraryAndVersion>());
}
webappLibraries.get(applicationName).add(libraryAndVersion);
}
private static void addLibrary(String libraryName, String libraryVersion, String filename)
throws MalformedURLException {
LibraryAndVersion libraryAndVersion = new LibraryAndVersion(libraryName, libraryVersion);
URL libraryLocation = new File(JARS_DIR + File.separator + filename).toURI().toURL();
libraryToClassLoader.put(libraryAndVersion,
new JarFileClassLoader("JarFileClassLoader for lib: " + libraryAndVersion,
new URL[] { libraryLocation }));
}
private LibrariesStorage() {
}
public static Class<?> loadClassForWebapp(String webappName, String className) throws ClassNotFoundException {
System.out.println("Loading class: " + className + " for web application: " + webappName);
List<LibraryAndVersion> webappLibraries = LibrariesStorage.webappLibraries.get(webappName);
for (LibraryAndVersion libraryAndVersion : webappLibraries) {
JarFileClassLoader libraryClassLoader = libraryToClassLoader.get(libraryAndVersion);
try {
return libraryClassLoader.loadClass(className); // ok current lib contained class to load
} catch (ClassNotFoundException e) {
// ok.. continue in loop... try to load the class from classloader connected to next library
}
}
throw new ClassNotFoundException("Class " + className + " was not found in any jar connected to webapp: " +
webappLibraries);
}
}
class LibraryAndVersion {
private final String name;
private final String version;
LibraryAndVersion(String name, String version) {
this.name = name;
this.version = version;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
LibraryAndVersion that = (LibraryAndVersion) o;
if ((name != null) ? (!name.equals(that.name)) : (that.name != null)) {
return false;
}
if ((version != null) ? (!version.equals(that.version)) : (that.version != null)) {
return false;
}
return true;
}
#Override
public int hashCode() {
int result = (name != null) ? name.hashCode() : 0;
result = (31 * result) + ((version != null) ? version.hashCode() : 0);
return result;
}
#Override
public String toString() {
return "LibraryAndVersion{" +
"name='" + name + '\'' +
", version='" + version + '\'' +
'}';
}
}
JBoss has a framework called Modules that solves this problem. You can save the shared library with its version and reference it from your war-file.
I have no idea if it works on Tomcat, but it works as a charm on Wildfly.