Meglévő technológiák OSGi integrációja

Előadó: Csákány Róbert

Bemutatkozás

  • 2009 óta foglalkozom OSGi-al

  • Sikeres enterprise alkalmazásfejlesztés (AVON Cosmetics MAPS rendszer)

  • OSGi alapú keretrendszer fejlesztés (JUDO)

  • Nyílt forráskódú fejlesztés (liveSense)

  • OSGi tanácsadás GE HealthCare részére

  • Github: https://github.com/robertcsakany

A témák

  • Az esszencia: Class Loading mechanizmus

  • Nem minden JAR OSGI bundle

  • Bundle is meg nem is...

  • Hogyan érdemes OSGi-ban fejleszteni?

Az esszencia: ClassLoading mechanizmus

Mitől lesz bundle a JAR-ból? - MANIFEST.MF file

Manifest-Version: 1.0
Bnd-LastModified: 1466006904163
Build-Jdk: 1.7.0_79
Built-By: robson
Bundle-Category: judo
Bundle-Description: Superpom: contains the configuration of the maven pl
 ugins.
Bundle-ManifestVersion: 2
Bundle-Name: rdbms-hsqldb-server
Bundle-SymbolicName: hu.blackbelt.judo.rdbms.hsqldb-server
Bundle-Version: 1.0.0.SNAPSHOT
Created-By: Apache Maven Bundle Plugin
Export-Package: hu.blackbelt.rdbms.hsqldb.server;uses:="org.osgi.service
 .cm,org.osgi.service.component,org.slf4j";version="1.0.0"
Import-Package: org.apache.sling.commons.osgi;version="[2.2,3)",org.hsql
 db,org.osgi.service.cm;version="[1.4,2)",org.osgi.service.component;ver
 sion="[1.2,2)",org.slf4j;version="[1.7,2)",org.joda.time.convert;versio
 n="[2.8,3)",org.joda.time.base;version="[2.8,3)",org.joda.time.tz;versi
 on="[2.8,3)",org.joda.time.chrono;version="[2.8,3)",org.joda.time.field
 ;version="[2.8,3)",org.joda.time.format;version="[2.8,3)"
Judo-Platform: true
Require-Capability: osgi.service;effective:=active;filter:="(objectClass
 =org.osgi.service.cm.ConfigurationAdmin)",osgi.ee;filter:="(&(osgi.ee=J
 avaSE)(version=1.7))"
Service-Component: OSGI-INF/hu.blackbelt.rdbms.hsqldb.server.HsqldbServe
 r.xml
Tool: Bnd-2.3.0.201405100607

Az esszencia: ClassLoading mechanizmus

Az esszencia: ClassLoading mechanizmus

Nincs egyésges recept a csomagolásra

Nem minden JAR OSGi bundle

  • Minden JAR külön bundle-ba kerül - OSGi filózófiához ez áll legközelebb. - Import exportok kezelése.

  • Több JAR-t csomagolhatunk egy bundle-ba - A bundlen belül reflection-el történik az osztályok töltése és statikus modellben került a rendszer felépítésre.

  • Proxy Classloader használata (Apache Sling JSP)

  • Dynamic import használata - mikor már más lehetőségünk nincs

  • A Bundle-ban a Factory class-ok felüldefiniálása

OSGI metaadatok hozzáadása MAVEN-el

Nem minden JAR OSGi bundle

	    <plugin>
		<groupId>org.apache.felix</groupId>
		<artifactId>maven-bundle-plugin</artifactId>
		<version>2.5.3</version>
		<extensions>true</extensions>
		<configuration>
		    <instructions>
                        <_exportcontents>
                            org.apache.poi.*;version=${poi.version},
                            org.openxmlformats.schemas.*;version=${poi.schema.version},
                            schemasMicrosoftComOfficeExcel.*;version=${poi.schema.version},
                            schemasMicrosoftComOfficeOffice.*;version=${poi.schema.version},
                            schemasMicrosoftComOfficePowerpoint.*;version=${poi.schema.version},
                            schemasMicrosoftComVml.*;version=${poi.schema.version},
                            org.etsi.uri.*;version=${poi.security.version}
                        </_exportcontents>
                        <Import-Package>
                            com.sun.javadoc;resolution:=optional,
                            com.sun.tools.javadoc;resolution:=optional,
                            org.apache.crimson.jaxp;resolution:=optional,
                            org.apache.tools.ant;resolution:=optional,
                            org.apache.tools.ant.taskdefs;resolution:=optional,
                            org.apache.tools.ant.types;resolution:=optional,
                            junit.framework.*;resolution:=optional,
                            junit.textui.*;resolution:=optional,
                            org.junit.*;resolution:=optional,
                            org.apache.xml.security.*;resolution:=optional,
                            org.apache.jcp.xml.dsig.internal.dom.*;resolution:=optional,
                            *
                        </Import-Package>
                        <DynamicImport-Package>
                            org.apache.xmlbeans.*,
                            schemaorg_apache_xmlbeans.*
                        </DynamicImport-Package>

			<!-- bundle supplied resource prefixes -->
			<Include-Resource>{maven-resources}</Include-Resource>

			<!-- Do not inline jars, include as jar files -->
			<!-- There are config files with same name will be overwritten -->
			<Embed-Dependency>*;scope=compile;inline=false</Embed-Dependency>


		    </instructions>
		</configuration>
	    </plugin>

Bundle is meg nem is...

  • WSO2 - Minden extension mint fragment bundle telepthető - Nincsenek service-k, nincsenek igazi importok / exportok

  • JBoss Activity - ugyanazok a packagek több JAR-ban megtalálhatóak. OSGI támogatja a split-package-t, de használata nem ajánlott.

  • Eclipse pluginek rendszeresen Required-Bundle-t használnak

  • Számtalan bundle ClassNotFound problémára a Dynamic Import Package-t haszmálja

Nem mindenki használja az OSGi-t az ajánlásoknak megfelelően

Hogyan érdemes csinálni?

Hogyan érdemes OSGi-ba szervezni

  • 3rd paty bundle - Ez tartalmazza 1 vagy több bundleban a 3rd party classokat illetve exportálja azokat a packageket, amelyek a használathoz kellenek. Általában service regisztrációkat nem tartalmaznak, de aktivátorokat tartalmazhat, például regisztrációk Factory-ba.
  • API Bundle - Az interface-k kódját tartalmazza, amely már egy absztraktabb formája az integrált frameworknek. Amennyiben már maga a framework külön definiálja az API interfaceit számunkra megfelelő formában, akkor a saját API bundle nem szükéseges.

Hogyan érdemes OSGi-ba szervezni

  • Service Bundle - Ez tartalmazza az API implementációját vagy azt a kódot, ami az API interface-re regisztrálja a kipléldányostott osztályt. Deklaratív service esetén ezt a Service Component Library végzi a ConfigAdmin servicbe regisztált konfiguráció alapján - ez kerül a ServiceRegistry-be.
  • Configok - Ezek alapértelmezetten az osztályok full qualified neve alapján képzett CFG fileok.

Hogyan érdemes OSGi-ba szervezni

<plugin>
	<groupId>org.apache.felix</groupId>
	<artifactId>maven-bundle-plugin</artifactId>
	<extensions>true</extensions>
	<configuration>
		<instructions>

			<Export-Package>
				com.octo.captcha.*;version:=1.0
			</Export-Package>

			<Embed-Dependency>
				jcaptcha,
				jcaptcha-api,
				filters,
				imaging
			</Embed-Dependency>

			<Import-Package>
				EDU.oswego.*;resolution:=optional,
				com.sun.speech.*;resolution:=optional,
				net.sf.ehcache.*;resolution:=optional,
				org.acegisecurity.*;resolution:=optional,
				org.apache.struts.*;resolution:=optional,
				org.apache.velocity.*;resolution:=optional,
				org.quartz.*;resolution:=optional,
				org.roller.*;resolution:=optional,
				org.springframework.*;resolution:=optional,
				*
			</Import-Package>

		</instructions>
	</configuration>
</plugin>

3rd party bundle

Hogyan érdemes OSGi-ba szervezni

package org.liveSense.service.captcha;

import java.awt.image.BufferedImage;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface CaptchaService {
		
    public BufferedImage getCaptchaImage(String captchaID, Locale locale);
    
    public boolean validateCapthaResponse(String captchaID, String response);

    public boolean validateCaptchaResponse(HttpServletRequest request, String response);

    public String extractCaptchaIdFromRequest(HttpServletRequest request);

    public void setCaptchaId(HttpServletRequest request,
	    HttpServletResponse response, String captchaId);

    public BufferedImage getCaptchaImage(String captchaEngineName, String captchaID, Locale locale);
    
    public boolean validateCaptchaResponse(String captchaEngineName, String captchaID, String response);

    public boolean validateCaptchaResponse(String captchaEngineName, HttpServletRequest request, String response);
    
    public void setCaptchaId(String captchaEngineName, HttpServletRequest request,
	    HttpServletResponse response, String captchaId);

}

API bundle

Hogyan érdemes OSGi-ba szervezni

package org.liveSense.service.captcha;

import java.awt.image.BufferedImage;
import java.util.Locale;

public interface CaptchaEngine {
	
	public boolean validateResponse(String id, String response);
	
	public BufferedImage getImage(String id, String text, Locale locale);
	
	public void init();
	
	public void close();

	public String getName();
}

API bundle

Hogyan érdemes OSGi-ba szervezni

@Component(label = "%captcha.service.name", description = "%captcha.service.description", immediate = false, metatype = true)

@Properties(value={
		@Property(label="%captcha.service.defaultengine", description="%captcha.service.defaultengine.description", name = CaptchaServiceImpl.PAR_CAPTHA_ENGINE, value = CaptchaServiceImpl.DEFAULT_CAPTCHA_ENGINE),
		@Property(label="%captcha.service.storage",description="%captcha.service.storage.description", name = CaptchaServiceImpl.PAR_STORAGE, value = CaptchaServiceImpl.DEFAULT_STORAGE, options = {
				@PropertyOption(name = CaptchaServiceImpl.STORAGE_COOKIE, value = "Cookie"),
				@PropertyOption(name = CaptchaServiceImpl.STORAGE_SESSION_ATTRIBUTE, value = "Session Attribute") 
		})
})

@References(value={
		@Reference(cardinality=ReferenceCardinality.MANDATORY_MULTIPLE, policy=ReferencePolicy.DYNAMIC, strategy=ReferenceStrategy.EVENT, bind="bindEngine", unbind="unBindEngine", referenceInterface=CaptchaEngine.class)
})
@Service
public class CaptchaServiceImpl implements CaptchaService {

	Logger log = LoggerFactory.getLogger(CaptchaServiceImpl.class);

	/**
	 * The value of the parameter indicating the use of a Cookie to store the
	 * authentication data.
	 */
	public static final String STORAGE_COOKIE = "cookie";

	/**
	 * The value of the parameter indicating the use of a session attribute to
	 * store the authentication data.
	 */
	public static final String STORAGE_SESSION_ATTRIBUTE = "session";

	/**
	 * To be used to determine if the chalange ID comes from a cookie or from a
	 * session attribute.
	 */
	public static final String DEFAULT_STORAGE = STORAGE_COOKIE;
	public static final String PAR_STORAGE = "captcha.storage";

	public static final String DEFAULT_CAPTCHA_ENGINE = "JCaptchaDefault";
	public static final String PAR_CAPTHA_ENGINE = "captcha.engine.default";

	public static final String PAR_CAPTCHA_ATTRIBUTE_NAME = "captcha.uid";

	private String authStorageType = DEFAULT_STORAGE;
	private String defaultCapthaEngineName = DEFAULT_CAPTCHA_ENGINE;

	@Reference(cardinality=ReferenceCardinality.MANDATORY_UNARY, policy=ReferencePolicy.DYNAMIC)
	Configurator configurator;

	CaptchaEngine defaultEngine;

	Map<String, CaptchaEngine> captchaEngines = new ConcurrentHashMap<String, CaptchaEngine>();

	protected void bindEngine(CaptchaEngine engine) {
		log.info("Binding CaptcheEngine: "+engine.getName());
		if (defaultCapthaEngineName.equals(engine.getName())) {
			defaultEngine = engine;
		}
		captchaEngines.put(engine.getName(), engine);

	}

	protected void unBindEngine(CaptchaEngine engine) {
		log.info("UnBinding CaptcheEngine: "+engine.getName());
		captchaEngines.remove(engine.getName());
		if (defaultCapthaEngineName.equals(engine.getName())) {
			defaultEngine = null;
		}

	}

	@Activate
	protected void activate(ComponentContext componentContext) {
		authStorageType = PropertiesUtil.toString(componentContext.getProperties().get(PAR_STORAGE), DEFAULT_STORAGE);
		defaultCapthaEngineName = PropertiesUtil.toString(componentContext.getProperties().get(PAR_CAPTHA_ENGINE), DEFAULT_CAPTCHA_ENGINE);
	}

	@Deactivate
	protected void deactivate(ComponentContext componentContext) {
	}

	private CaptchaEngine getDefaultEngine() {
		if (defaultEngine != null) {
			return defaultEngine;
		} else {
			log.warn("No default engine is defined, we use the first service");
			for (Map.Entry<String, CaptchaEngine> entry : captchaEngines.entrySet()) {
				return entry.getValue();
			}
		}
		return null;
	}
	
	private CaptchaEngine getCapthaEngineByName(String engineName) {
		if (engineName == null) {
			return getDefaultEngine();
		}
		if (captchaEngines.containsKey(engineName)) {
			return captchaEngines.get(engineName);
		} else {
			log.warn("Could not found CaptchaEngine in the given name: "+engineName);
			return getDefaultEngine();
		}
	}

	private BufferedImage getCaptchaImage(CaptchaEngine engine, String captchaId, Locale locale) {
		return engine.getImage(captchaId, null, locale);
	}

	private boolean validateCaptchaResponse(CaptchaEngine engine, String captchaId, String response) {
		return engine.validateResponse(captchaId, response);
	}

	private boolean validateCaptchaResponse(CaptchaEngine engine, HttpServletRequest request, String response) {
		return validateCaptchaResponse(engine, extractCaptchaIdFromRequest(request), response);
	}

	public void setCaptchaId(CaptchaEngine engine, HttpServletRequest request,
			HttpServletResponse response, String captchaId) {
		if (authStorageType.equals(STORAGE_COOKIE)) {
			Cookie[] cookies = request.getCookies();
			boolean foundCookie = false;

			if (cookies != null) {
				for (int i = 0; i < cookies.length; i++) {
					Cookie cookie = cookies[i];
					if (cookie.getName().equals(PAR_CAPTCHA_ATTRIBUTE_NAME)) {
						foundCookie = true;
						cookie.setMaxAge(configurator.getSessionTimeout().intValue());
						cookie.setValue(captchaId);
						cookie.setPath("/");
						response.addCookie(cookie);
					}
				}
			}

			if (!foundCookie) {
				Cookie cookie = new Cookie(PAR_CAPTCHA_ATTRIBUTE_NAME,
						captchaId);
				cookie.setMaxAge(configurator.getSessionTimeout().intValue());
				cookie.setPath("/");
				response.addCookie(cookie);
			}

		} else {
			if (request.getSession() == null) {
				request.getSession(true).setMaxInactiveInterval(
						configurator.getSessionTimeout().intValue());
				request.getSession().setAttribute(PAR_STORAGE, captchaId);
				captchaId = request.getSession().getId();
			}

		}
	}

	@Override
	public BufferedImage getCaptchaImage(String captchaEngineName,
			String captchaID, Locale locale) {
		return getCaptchaImage(getCapthaEngineByName(captchaEngineName), captchaID, locale);
	}

	@Override
	public boolean validateCaptchaResponse(String captchaEngineName,
			String captchaID, String response) {
		return validateCaptchaResponse(getCapthaEngineByName(captchaEngineName), captchaID, response);
	}

	@Override
	public boolean validateCaptchaResponse(String captchaEngineName,
			HttpServletRequest request, String response) {
		return validateCaptchaResponse(getCapthaEngineByName(captchaEngineName), request, response);
	}

	@Override
	public void setCaptchaId(String captchaEngineName,
			HttpServletRequest request, HttpServletResponse response,
			String captchaId) {
		setCaptchaId(getCapthaEngineByName(captchaEngineName), request, response, captchaId);
	}

	@Override
	public BufferedImage getCaptchaImage(String captchaID, Locale locale) {
		return getCaptchaImage(getDefaultEngine(), captchaID, locale);
	}

	@Override
	public boolean validateCapthaResponse(String captchaID, String response) {
		return validateCaptchaResponse(getDefaultEngine(), captchaID, response);
	}

	@Override
	public boolean validateCaptchaResponse(HttpServletRequest request,
			String response) {
		return validateCaptchaResponse(getDefaultEngine(), request, response);
	}

	@Override
	public void setCaptchaId(HttpServletRequest request,
			HttpServletResponse response, String captchaId) {
		setCaptchaId(getDefaultEngine(), request, response, captchaId);
	}

	@Override
	public String extractCaptchaIdFromRequest(HttpServletRequest request) {
		String captchaId = null;
		if (authStorageType.equals(STORAGE_COOKIE)) {
			captchaId = UUID.randomUUID().toString();
			Cookie[] cookies = request.getCookies();
			for (int i = 0; i < cookies.length; i++) {
				Cookie cookie = cookies[i];
				if (cookie.getName().equals(PAR_CAPTCHA_ATTRIBUTE_NAME)) {
					captchaId = cookie.getValue();
				}
			}

		} else {
			if (request.getSession() != null) {
				captchaId = (String)request.getSession().getAttribute(PAR_CAPTCHA_ATTRIBUTE_NAME);
			}
		}
		return captchaId;
	}

}

Service bundle

Hogyan érdemes OSGi-ba szervezni


class JCaptchaBasedDefaultCaptchaEngineParameterProvider {
	public static final String PROP_ENGINE_NAME = "engine.name";
	public static final String DEFAULT_ENGINE_NAME = "JCaptchaDefault";

	public static String getEngineName(ComponentContext context) {
		return PropertiesUtil.toString(context!=null?context.getProperties().get(PROP_ENGINE_NAME):DEFAULT_ENGINE_NAME, DEFAULT_ENGINE_NAME);
	}
}

@Component(label = "%jcaptchadefault.service.name", description = "%jcaptchadefault.service.description", immediate = true, metatype = true)

@Properties(value={
		@Property(label="%jcaptchadefault.name", description="%jcaptchadefault.name.description", name = JCaptchaBasedDefaultCaptchaEngineParameterProvider.PROP_ENGINE_NAME,  value=JCaptchaBasedDefaultCaptchaEngineParameterProvider.DEFAULT_ENGINE_NAME)
})
@Service
public class JCaptchaBasedDefaultCaptchaEngine implements CaptchaEngine {	
	private DefaultManageableImageCaptchaService instance;
	private String name = JCaptchaBasedDefaultCaptchaEngineParameterProvider.DEFAULT_ENGINE_NAME;
			
	@Override
	public BufferedImage getImage(String id, String text, Locale locale) {
		BufferedImage ret = null;
		for (int i=0; i<3; i++) {
			try {
				ret = instance.getImageChallengeForID(id, locale);
				return ret;
			} catch (CaptchaServiceException e) {
			}
		}
		return instance.getImageChallengeForID(id, locale);
	}

	@Override
	public void init() {
		instance = new DefaultManageableImageCaptchaService(
				new FastHashMapCaptchaStore(), new DefaultGimpyEngine(), 180,
				100000, 75000);
	}

	@Override
	public void close() {
		if (instance != null)
			instance.emptyCaptchaStore();
	}

	@Override
	public boolean validateResponse(String id, String response) {
		return instance.validateResponseForID(id, response);
	}
	

	@Activate
	protected void activate(ComponentContext context) {
		name = JCaptchaBasedDefaultCaptchaEngineParameterProvider.getEngineName(context);
		init();
	}
	
	@Deactivate
	protected void deactivate() {
		close();
	}

	@Override
	public String getName() {
		return name;
	}
}

Service bundle

Hogyan érdemes OSGi-ba szervezni


/**
 * The <code>CaptchaServlet</code> returns a captcha image and set sessionID for captcha 
 * servlet.
 */
@Component(label="%captcha.servlet.name", description="%captcha.servlet.description", immediate=true, metatype=true)
@Service
@Properties(value={
		@Property(label="%captcha.servlet.path", description="%captcha.servlet.pathdescription", name="sling.servlet.paths", value={"/session/captcha.jpg", "/session/captcha.png"}),
		@Property(name="sling.servlet.methods", value={"GET"}, propertyPrivate=true)
})
public class CaptchaServlet extends SlingAllMethodsServlet {



	private static final long serialVersionUID = -2160335731233369891L;

	@Reference(cardinality=ReferenceCardinality.MANDATORY_UNARY, policy=ReferencePolicy.DYNAMIC)
	CaptchaService captcha;


	/** default log */
	private final Logger log = LoggerFactory.getLogger(CaptchaServlet.class);

Consumer bundle

Hogyan érdemes OSGi-ba szervezni

Config

org.liveSense.service.captcha.impl.CaptchaServiceImpl.cfg

captcha.storage=cookie
captcha.engine.default=JCaptchaDefault

org.liveSense.service.captcha.jcaptcha.impl.JCaptchaBasedDefaultCaptchaEngine.cfg

engine.name=JCaptchaDefault

org.liveSense.service.captcha.impl.CaptchaServiceImpl.cfg

captcha.servlet.path=/captcha/captcha.png

Kérdések?

Köszönöm a figyelmet!

Meglévő technológiák OSGi integrációja

By Róbert Csákány

Meglévő technológiák OSGi integrációja

Bemutatásra kerül, hogy nem OSGi-ra tervezett alkalmazások hogyan integrálhatóak OSGi környezetbe. Mivel nincsen rá általános recept, hogyan lehet a különböző tervezésbeli eltéréseket áthidalni, néhány hasznos gyakrolat kerül megosztásra illetve hibás minták, melyeket érdemes elkerülni.

  • 780