Zapoznamy się teraz z koncepcją lekkich kontenerów, wzorcem projektowym "dependency injection" oraz - m.in. opartymi na nich elementami specyfikacji EJB 3.0 oraz Java Persistance API.
Będzie to naturalnie tylko wprowadzenie do rozległej tematyki EJB 3.0, ale za to nastawione praktycznie - będziemy testować przykładowe programy w środowisku pierwszego serwera aplikacji implementującego specyfikację EJB 3.0 - Sun Java Application Server 9.
List list = new ArrayList();
List list = ListFactory.createList();
List list; // po iniekcji list wskazuje na obiekt implementujący interfejs List // trzeba jeszcze powiedzieć jakoś, że w tym miejscu jest potrzebna iniekcjaIniekcja za pośrednictwem konstruktora:
class A { private List list; public A(List l) { // iniekcja dokonana za pośrednictwem konstruktora list = l; // konstruktorowi przekazywany jest obiekt klasy implementującej List } }
class A { private List list; public void setList(List l) { // iniekcja poprzez setter list = l; } }
import javax.ejb.EJBObject; import java.rmi.RemoteException; import java.math.*; public interface Pit extends EJBObject { public BigDecimal taxToPay(BigDecimal income) throws RemoteException; }
import java.math.BigDecimal; import javax.ejb.Remote; @Remote public interface Pit { public BigDecimal taxToPay(BigDecimal income); }
import javax.ejb.SessionBean; import javax.ejb.SessionContext; import java.math.*; public class PitBean implements SessionBean { private BigDecimal taxRate = new BigDecimal("0.14"); public PitBean() { } public BigDecimal taxToPay(BigDecimal income) { BigDecimal result = income.multiply(taxRate); return result.setScale(2, BigDecimal.ROUND_UP); } // Metoda wołana przy tworzeniu obiektu ziarna public void ejbCreate() { } // Metody interfejsu SessionBean public void ejbRemove() { } public void ejbActivate() { } public void ejbPassivate() { } public void setSessionContext(SessionContext sc) { } }
package pit.ejb; import java.math.BigDecimal; import javax.ejb.*; @Stateless public class PitBean implements Pit { private BigDecimal taxRate = new BigDecimal("0.14"); public PitBean() { } public BigDecimal taxToPay(BigDecimal income) { BigDecimal result = income.multiply(taxRate); return result.setScale(2, BigDecimal.ROUND_UP); } }
public interface Pit { //... } @Stateless @Remote public PitBean implements Pit { // ... }Niektóre serwery aplikacji (JBoss, Resin) potrafią także generować niezbędne interfejsy na podstawie adnotacji @BusinessMethod w klasie komponentu.
import javax.naming.Context; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import java.math.BigDecimal; public class PitClient { public static void main(String[] args) { try { Context initial = new InitialContext(); Context myEnv = (Context) initial.lookup("java:comp/env"); Object objref = myEnv.lookup("ejb/Pit"); PitHome home = (PitHome) PortableRemoteObject.narrow(objref, PitHome.class); Pit pit = home.create(); BigDecimal income = new BigDecimal("100000.00"); BigDecimal tax = pit.taxToPay(income); System.out.println("Podatek wynosi: " + tax); System.exit(0); } catch (Exception ex) { System.err.println("Caught an unexpected exception!"); ex.printStackTrace(); } } }I na dodatek dostarczyć odpowiedniego deskryptora wdrożenia.
Pit pit; // .... try { InitialContext ic = new InitialContext(); pit = (Pit) ic.lookup("pit.ejb.Pit"); } catch (NamingException e) { e.printStackTrace(); } BigDecimal income = new BigDecimal("100000.00"); BigDecimal tax = pit.taxToPay(income); System.out.println("Podatek wynosi: " + tax);Zwróćmy uwagę, że forma nazwy JNDI dla zdalnego obiektu biznesowego jest zależna od serwera aplikacji. W podanym przykładzie pit.ejb.Pit jest nazwą automatycznie generowaną przez Sun Application Server 9 dla typu Pit umieszczonego w pakiecie pit.ejb.
public class PitClient { @EJB private static Pit pit; // iniekcja obiektu zdalnego public static void main(String[] args) { BigDecimal income = new BigDecimal("100000.00"); BigDecimal tax = pit.taxToPay(income); System.out.println("Podatek wynosi: " + tax); } }Uwaga: powyżej pole pit musi być statyczne, ponieważ iniekcja następuje podczas wywołania statycznej metody main.
@Resource SessionContext ctx; @Resource (name="jdbc/SomeDB") DataSource myDb; @Resource (name="ConnectionFactory") QueueConnectionFactory factory; @Resource (name="queue/A") Queue queue;
@Resource(name="jdbc/JakasBD") public void setDataSource(DataSource ds) this.ds = da; }To samo dotyczy adnotacji @EJB.
@Stateless @Interceptors{ (Test.class) } public class SlesBean implements BusInterf { //... }W klasach tych metody przechwytujące oznaczamy adnotacją @AroundInvoke.
public class Test { @AroundInvoke public Object testArgsAndAddTxtToResult(InvocationContext ic) throws Execption { // ... }Interfejs InvocationContext dostarcza metod manipulacji na argumentach wywołania i otrzymanych wynikach.
@Entity @Table(name="SCHOOL_STUDENT") // nazwa tabeliw bazie danych public class Student implements Serializable { private String id; private String name; public Student() { } public Student(String index, String name) { this.id = index; this.name = name; } @Id // klucz główny public String getId() { return id; } public void setId(String index) { this.id = index; } public String getName() { return name; } public void setName(String name) { this.name = name; } }Serwer aplikacji odwzoruje tabelę SCHOOL_STUDENT z domyślnymi nazwami kolumn ID i NAME na obiekty klasy Student. A nawet - przy odpowiednio ustawionych opcjach - utworzy taką tabelę jeśli jej nie ma. Możemy też dostosować nazwy kolumn za pomocą odpowiednich adnotacji @Column stawianych przed getterami.
@PersistenceContext EntityManager em;
@PersistenceContext EntityManager em; // ... Student s = new Student("s0001", "Kowalski Jan"); em.persist(s); // .... Student p = em.find(Student.class, "s0001"); em.remove(p);Uwaga: pominęto obsługę wyjątków
public List<Student> findStudents(String name) { return em.createQuery( "SELECT s FROM Student s WHERE s.name LIKE :studName") .setParameter("studName", name) .getResultList(); }Tutaj createQuery() tworzy obiekt typu Query. Jest to zapytanie z parametrem o nazwie studName. Za pomocą metody setParameter (wołanej na rzecz Query) ustalamy wartość parametru. Metoda getResultList() zwraca listę wyników zapytania.
@NamedQuery( name="getAllStudents", query="SELECT s FROM Student s" )i używamy za pomocą metody createNamedQuery EntityManagera, np.:
List<Student> result = em.createNamedQuery("getAllStudents").getResultList();
package pit.common; import java.math.BigDecimal; public interface Pit { public BigDecimal taxToPay(BigDecimal income); }
package pit.ejb; import java.math.BigDecimal; import javax.ejb.*; import pit.common.*; @Stateless @Remote public class PitBean implements Pit { private BigDecimal taxRate = new BigDecimal("0.14"); public PitBean() { } public BigDecimal taxToPay(BigDecimal income) { BigDecimal result = income.multiply(taxRate); return result.setScale(2, BigDecimal.ROUND_UP); } }
import javax.naming.*; import javax.swing.*; import pit.common.*; @SuppressWarnings("serial") public class PitClient extends JFrame implements ActionListener { @EJB private static Pit pit; private JTextField intf = new JTextField(10); private JLabel result = new JLabel(); public PitClient() { this("DEFAULT_CONTEXT"); } public PitClient(String appType) { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); init(appType); setTitle("Tax calculator"); intf.setBorder(BorderFactory.createTitledBorder("Income")); result.setPreferredSize(intf.getPreferredSize()); result.setOpaque(true); result.setBackground(Color.WHITE); result.setForeground(Color.RED); result.setBorder(BorderFactory.createTitledBorder("Tax to pay")); setLayout(new FlowLayout()); JButton calc = new JButton("Calc"); add(intf); add(calc); add(result); intf.addActionListener(this); calc.addActionListener(this); pack(); this.setLocationRelativeTo(null); setVisible(true); } public void actionPerformed(ActionEvent e) { result.setText(""); BigDecimal income = null; String in = intf.getText().trim(); try { income = new BigDecimal(in); } catch (NumberFormatException exc) { JOptionPane.showMessageDialog(null, "Invalid income: " + in); return; } BigDecimal tax = pit.taxToPay(income); result.setText(tax.toString()); } private void init(String type) { if (type.equals("JAVA_APP")) pit = new pit.ejb.PitBean(); else if (type.equals("JAVA_CLIENT")) { try { InitialContext ic = new InitialContext(); pit = (Pit) ic.lookup("pit.common.Pit"); } catch (NamingException e) { e.printStackTrace(); } } } public static void main(String[] args) { if (args.length >= 1) new PitClient(args[0]); else new PitClient(); } }Oczywiście, aplikację nie wystarczy skompilowac, trzeba ją również wdrożyć.
javaee.home= ... tu trzeba podać ścieżkę do instalacji SJAS 9 javaee.server.name=localhost javaee.server.port=8080 javaee.adminserver.port=4848 javaee.server.username=admin asadmin=asadmin.batPlik specyficznych właściwości projektu - build.props:
app=Pit1 // nazwa aplikacji pack=pit // pakiet has.ejb=yes // czy ma moduł EJB has.clientjar=yes // czy ma JAR klienta has.javaclient=yes // czy zrobić wolnostojącego klienta has.common=yes // czy ma klasy dzielone pomiędzy moduły main.class=pit.client.PitClient // nazwa głównej klasy klientaGeneralny skrypt Ant - defbuild.xml
<!-- Konfiguracja wlasciwosci --> <property file="../common/common.properties"/> <property file="build.props"/> <target name="prepare" depends="init"> <mkdir dir="dist"/> <delete> <fileset dir="dist" includes="**/*.jar **/*.war **/*.ear"/> </delete> </target> <target name="ejb" if="has.ejb" depends="prepare"> <jar jarfile="dist/${app}Ejb.jar"> <fileset dir="bin" includes="${pack}/ejb/**.*class"/> <fileset dir="bin/${pack}/ejb" includes="META-INF/**.*"/> </jar> </target> <target name="war" if="has.war" depends="prepare"> <delete dir="webbuild"/> <mkdir dir="webbuild"/> <mkdir dir="webbuild/WEB-INF"/> <mkdir dir="webbuild/WEB-INF/classes"/> <copy todir="webbuild"> <fileset dir="bin/${pack}/web" excludes="**.*class"/> </copy> <copy todir="webbuild/WEB-INF/classes"> <fileset dir="bin" includes="${pack}/web/**.*class"/> </copy> <jar jarfile="dist/${app}.war" basedir= "webbuild"/> </target> <target name="client-jar" if="has.clientjar" depends="prepare"> <manifest file="MANIFEST.MF"> <attribute name="Main-Class" value="${main.class}"/> </manifest> <jar jarfile="dist/${app}Client.jar" manifest="MANIFEST.MF"> <fileset dir="bin" includes="${pack}/client/**.*"/> </jar> <delete file="MANIFEST.MF"/> </target> <target name="prepare-lib"> <delete dir="dist/lib"/> <mkdir dir= "dist/lib"/> </target> <target name="common" if="has.common" depends="prepare,prepare-lib"> <jar jarfile="dist/lib/${app}Common.jar"> <fileset dir="bin" includes="${pack}/common/**.*class"/> </jar> </target> <target name="entity" if="has.entity" depends="prepare,prepare-lib"> <jar jarfile="dist/lib/${app}Entity.jar"> <fileset dir="bin" includes="${pack}/entity/**.*class"/> <fileset dir="bin/${pack}/entity" includes="META-INF/**.*"/> </jar> </target> <target name="java-client" if="has.javaclient" depends="prepare"> <mkdir dir="javaclient"/> <delete> <fileset dir="javaclient" includes="**/*.*"/> </delete> <copy todir="javaclient"> <fileset dir="bin" includes="${pack}/client/**.*class"/> <fileset dir="bin" includes="${pack}/common/**.*class"/> <fileset dir="bin" includes="${pack}/entity/**.*class"/> </copy> <echo file="javaclient/runClient.bat" append="false" message="java -cp .;${javaee.home}/lib/appserv-rt.jar;${javaee.home}/lib/javaee.jar; ${main.class} JAVA_CLIENT"/> </target> <target name="dist" depends="ejb,entity,war,common,client-jar"> <jar jarfile="dist/${app}.ear" basedir="dist"/> </target> <target name="deploy" depends="java-client,dist"> <exec executable="${asadmin}"> <arg line=" deploy "/> <arg line=" --user ${javaee.server.username}" /> <arg line=" --passwordfile ../common/password.txt" /> <arg line=" --contextroot ${app}"/> <arg line=" --retrieve ."/> <arg line="dist/${app}.ear" /> </exec> </target>Jak widać tworzony jest tu katalog dist, w którym umieszczane są wszystkie JARy. Tworzone są też niezbędne katalogi dla klienta itp. Zadanie dist pakuje ostatecznie niezbędne modułu do pliku EAR. Zadanie deploy uruchamia instrukcję asadmin z opcją deploy, co powoduje wdrożenie aplikacji na serwerze. Podajemy przy tym hasło zapisane w pliku password.txt, a także żadamy załadowania jaru klicnckiego (opcja retrieve).
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html lang="pl"> <head> <title>WebStart Test</title> </head> <body> <h2 style="text-align: center;">Start klienta aplikacji Pit</h2> <br> <div style="text-align: center;"> <form method="get" action="http://localhost:8080/Pit1/Pit1Client" name="start"> <input name="b" value="Start aplikacji" type="submit"><br> </form> </div> </body> </html>Zobacz prezentację WebStartu.
java.naming.factory.initial=com.sun.enterprise.naming.SerialInitContextFactory java.naming.factory.url.pkgs=com.sun.enterprise.naming # Required to add a javax.naming.spi.StateFactory for CosNaming that # supports dynamic RMI-IIOP. java.naming.factory.state=com.sun.corba.ee.impl.presentation.rmi.JNDIStateFactoryImplWymienione klasy są w bibliotece javaee.jar - zatem i ją należy umieścić na ścieżce classpath.
setlocal set elib= ... tu katalog lib serwera aplikacji java -cp .;%elib%\appserv-rt.jar;%elib%\javaee.jar pit.client.PitClient JAVA_CLIENT endlocal
package school.entity; import java.io.*; import java.util.*; import javax.persistence.*; import static javax.persistence.CascadeType.REMOVE; @SuppressWarnings("serial") @Entity @Table(name="SCHOOL_STUDENT") @NamedQueries({ @NamedQuery(name = "getAllStudents",query = "SELECT s FROM Student s"), @NamedQuery(name = "getStudentsForCourse", query = "SELECT s FROM Student s, IN (s.courses) AS c WHERE c.id = :course_id" ) }) public class Student implements Serializable { private String id; private String name; private Collection<Course> courses; public Student() { } public Student(String index, String name) { this.id = index; this.name = name; } @Id public String getId() { return id; } public void setId(String index) { this.id = index; } @ManyToMany(cascade = REMOVE, mappedBy = "students") public Collection<Course> getCourses() { return courses; } public void setCourses(Collection<Course> courses) { this.courses = courses; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String toString() { return id + " " + name; } }
package school.entity; import java.io.*; import java.util.*; import javax.persistence.*; import static javax.persistence.CascadeType.REMOVE; @SuppressWarnings("serial") @Entity @Table(name="SCHOOL_COURSE") @NamedQueries({ @NamedQuery(name = "getAllCourses", query = "SELECT c FROM Course c" ), @NamedQuery(name = "getCoursesForStudent", query = "SELECT c FROM Course c, IN (c.students) AS s WHERE s.id = :stud_id" ) } ) public class Course implements Serializable{ private String id; private String name; private Set<Student> students; public Course() { } public Course(String id, String name) { this.id = id; this.name = name; } @Id public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @ManyToMany(cascade = REMOVE) @JoinTable(name = "SCHOOL_COURSE_STUDENT", joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"), inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID") ) public Set<Student> getStudents() { return students; } public void setStudents(Set<Student> students) { this.students = students; } public void addStudent(Student s) { getStudents().add(s); } public String toString() { return id + " " + name; } }W klasie Student relacja do kursów oznaczona jest adnotacją @ManyToMany. Opcja cascade określa sposób postępowania z powiązanymi rekordami (przy usuwaniu). Opcja mappedBy określa powiązanie z odpowiednim polem klasy Course (istotnie students są kolekcją studentów uczęszczających na dany kurs).
package school.common; import java.util.*; import javax.ejb.*; import school.entity.*; @Remote public interface DbAccess { void createCourse(String id, String name) throws SchoolDbException;; void createStudent(String id, String name) throws SchoolDbException; void assignCourseForStudent(String studId, String courseId) throws SchoolDbException; List<Student> getStudents(String courseId); List<Course> getCourses(String studId); }
package school.ejb; import java.util.*; import javax.ejb.*; import javax.persistence.*; import school.common.*; import school.entity.*; @Stateful public class DbAcessBean implements DbAccess { @PersistenceContext private EntityManager em; public void createCourse(String id, String name) throws SchoolDbException{ Course c = new Course(id, name); try { em.persist(c); } catch (Exception exc) { throw new SchoolDbException(exc + "\n" + exc.getCause()); } } public void createStudent(String id, String name) throws SchoolDbException { Student s = new Student(id, name); try { em.persist(s); } catch (Exception exc) { throw new SchoolDbException(exc + "\n" + exc.getCause()) ; } } public void assignCourseForStudent(String studId, String courseId) throws SchoolDbException { Student s = em.find(Student.class, studId); Course c = em.find(Course.class, courseId); if (s == null) throw new SchoolDbException("Invalid student id " + studId); if (c == null) throw new SchoolDbException("Invalid course id " + courseId); c.addStudent(s); } @SuppressWarnings("unchecked") public List<Student> getStudents(String courseId) { List<Student> result = null; if (courseId == null) result = em.createNamedQuery("getAllStudents").getResultList(); else result = em.createNamedQuery("getStudentsForCourse") .setParameter("course_id", courseId) .getResultList(); return result; } @SuppressWarnings("unchecked") public List<Course> getCourses(String studId) { List<Course> result = null; if (studId == null) result = em.createNamedQuery("getAllCourses").getResultList(); else result = em.createNamedQuery("getCourserForStudent") .setParameter("stud_id", studId) .getResultList(); return result; } }Po to, by móc obsługiwac wyjątki typu SQLException wprowadziliśmy własną klasę SchoolDbException.
package school.common; import java.io.*; @SuppressWarnings("serial") public class SchoolDbException extends Exception implements Serializable { public SchoolDbException() { super(); } public SchoolDbException(String message) { super(message); } public SchoolDbException(String message, Throwable cause) { super(message, cause); } public SchoolDbException(Throwable cause) { super(cause); } }Musi ona być serializowalna, aby móc uzyskać dostęp do wyjątku po stronie klienta.
package school.client; import java.util.*; import javax.ejb.EJB; import javax.naming.*; import javax.swing.*; import school.common.*; import school.entity.*; public class SchoolDbClient { @EJB private static DbAccess dba; public SchoolDbClient() { this("DEFAULT_CONTEXT"); } public SchoolDbClient(String appType) { init(appType); String[] studs = { "Kowalski Jan", "Kowalska Anna", "Malinowski Jan" }; try { for (int i = 0; i < studs.length; i++) { System.out.println("Create student " + i + "\n"); dba.createStudent("s000" + i, studs[i]); } String[] kurs = { "PRG", "WPR", "MPR" }; for (int i = 0; i < kurs.length; i++) { dba.createCourse(kurs[i], kurs[i] + " - opis"); } dba.assignCourseForStudent("s0000", "WPR"); dba.assignCourseForStudent("s0000", "MPR"); dba.assignCourseForStudent("s0001", "PRG"); dba.assignCourseForStudent("s0002", "PRG"); dba.assignCourseForStudent("s0002", "WPR"); dba.assignCourseForStudent("s0002", "MPR"); } catch (SchoolDbException exc) { exc.printStackTrace(); System.exit(1); } StringBuffer sb = new StringBuffer(); List<Student> s = dba.getStudents(null); for (Student info : s) sb.append(info).append('\n'); JOptionPane.showMessageDialog(null, sb.toString()); sb.setLength(0); s = dba.getStudents("WPR"); for (Student info : s) sb.append(info).append('\n'); JOptionPane.showMessageDialog(null, sb.toString()); } private void init(String type) { if (type.equals("JAVA_APP")) throw new RuntimeException("Java App not supported"); else if (type.equals("JAVA_CLIENT")) { try { InitialContext ic = new InitialContext(); dba = (DbAccess) ic.lookup("school.common.DbAccess"); } catch (NamingException e) { e.printStackTrace(); } } } public static void main(String[] args) { if (args.length >= 1) new SchoolDbClient(args[0]); else new SchoolDbClient(); } }
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="em" transaction-type="JTA"> <jta-data-source>jdbc/SchoolDB</jta-data-source> <properties> <property name="toplink.ddl-generation" value="drop-and-create-tables"/> </properties> </persistence-unit> </persistence>Zwróćmy uwagę na źródło danych. Odpowiednią nazwę JNDI musimy stworzyć np. z konsoli administracyjnej.