Sansür, bir toplumun kendine olan güvensizliğini yansıtır ve otoriter rejimlerin belirgin bir özelliğidir.

--Potter Stewart

24.12.2011

equals() Metodu Nasıl Yazılmalı

Yeni mezun meslektaşlarımızın List, Map, Set türevi veri yapılarında nesneleri konumlandırdıkları zaman nesnelerin equals ve hashCode metotlarını hazırlamamaları yaygın bir unutkanlıktır. List bileşenine koydukları nesne nedense contains() kontrolünde kaybolur. Sorunun çözümünü de benzer olayları önceden yaşamış ve kaynağını az çok tahmin eden tecrübeli yazılım geliştiricen gelir. equals ve hashCode metotlarının sınıf için gerçekleştirimi unutulmuştur. List ve Set sınıfları barındırdıkları nesnelerin eşitliğini bu iki metodu kullanarak kontrol ederler

Ne var ki equals ve hashCode metotları tecrübeli geliştiriciler tarafından bile olması gerektiği gibi hazırlanmıyor. Bu yazımda "Effective Java" kitabında yer alan "Item 8: Obey the general contract when overriding equals" bölümüne referans göstererek ideal bir equals metodunun nasıl olması gerektiğine değineceğim.

İlk uyulması gereken kural basit. "En iyi kod yazılmamış kodtur" ilkesini benimsemiş gibi: Eğer sınıfın her bir nesnesi sadece kendisiyle eşit olacak ise, yani sadece aynı bellek alanına referans gösteren değişkenler birbirleriyle eşit olacak ise sınıfta equals metodu yazmayın. Her Java sınıfının türediği Object sınıfının equals metodu bu bellek kontrolünü üstlenmiş durumda zaten. Böyle bir durumu oluşturabilecek senaryolar ise şu şekilde listeleniyor:


  • Sınıfın değerleri üzerinden işlevsel bir eşitlik gerektirmiyorsa: Örneğin java.util.Random sınıfının görevi rasgele sayılar üretmektir. İki Random sınıfının birbirine eşit olup olmaması işlevsel bir eşitlik kontrolü gerektirmez.
  • Atasınıfın (superclass) ihtiyacı karşılayacak şekilde gerçekleştirilmiş halihazırda bir equals metodu varsa: Çoğu Set gerçekleştirimi AbstractSet, List gerçekleştirimleri ise AbstractList atasınıfının equals metodunu kullanır.
  • Eğer sınıf private ve package-private ise ve sınıflar üzerinde eşitlik kontrolü yapılmayacağı kesinse
  • Sınıfın sadece yaratılmış tek bir olgusu olacak ise: Singleton sınıflar
Özetle sınıfımızın eşitliği, barındırdığı değişkenlerin değerleri üzerinden anlam ifade ediyorsa ve atasınıfta bu değerler üzerinden eşitlik kontrolünü sağlayacak metot yoksa sınıfımız için bir equals metodu yazmalıyız.

Peki ideal bir equals nasıl olmalı? Matematik derslerinden hatırlayacağımız aşağıdaki 5 kriteri de karşılamalı:

  • Dönüşlü: null olmayan her x değeri için x.equals(x)'in sonucu true olmalı.
  • Simetrik: null olmayan her x, y değerleri için x.equals(y) sadece y.equals(x) sonucu true ise true olmalı.
  • Geçişli: null olmayan her x, y, z değerleri için x.equals(y) ve y.equals(z) sonuçları true ise x.equals(z) sonucu da true olmalı.
  • Tutarlı: eşitlik kontrolünde kullanılan değişkenlerin değerleri değişmediği sürece x.equals(y) sonucu her zaman ya hep true ya da hep false olmalı.
  • null olmayan her x için x.equals(null) sonucu hep false olmalı
Bu eğlenceli matematiksel kriterleri sağlayamayan her equals metodu uygulamamızda sıkıntı yaratacak ve hatanın kaynağı bulmak da belki uzunca bir süre mümkün olmayacaktır. Bu kriterlere biraz yakından bakalım:


Dönüşlü
Basitçe, her bir nesne kendisine eşit olmalı. Bu kriteri bozacak bir gerçekleştirimde, List'e eklediğimiz nesnemizi contains metoduyla sorguladığımızda karşılaşacağımız sonuç olumsuz olacaktır.

Simetrik
Simetri kriterini aşağıdaki örnek kod kırıyor. (Örnek kodlar Effective Java kitabından alınmıştır):

// Broken - violates symmetry!
   public final class CaseInsensitiveString {
       private final String s;
       public CaseInsensitiveString(String s) {
           if (s == null)
               throw new NullPointerException();
           this.s = s;
}
       // Broken - violates symmetry!
       @Override public boolean equals(Object o) {
           if (o instanceof CaseInsensitiveString)
               return s.equalsIgnoreCase(
                   ((CaseInsensitiveString) o).s);
           if (o instanceof String)  // One-way interoperability!
               return s.equalsIgnoreCase((String) o);
           return false;
       }
       ...  // Remainder omitted
   }

 
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");String s = "polish";

 
Burada cis.equals(s) kontrolü true dönerken s.equals(cis) kontrolü ise false değerini dönecektir. Problemin kaynağı CaseInsensitiveString sınıfının equals metodu, karşılaştırmayı büyük/küçük harf ayırtmaksızın yaparken, String sınıfının equals metodunun bu ayrımı yapmamasından kaynaklanmaktadır.

List list = new ArrayList();
list.add(cis);

Yukarıdaki gibi bir senaryoda yapılan list.contains(s) kontrolü kodun üzerinde çalıştığı Java gerçekleştirimine göre true ya da false dönebilir. "Benim bilgisayarımda çalışıyordu" dememek için hastalıklı kodu düzeltmeliyiz. Karşılaştırmada String sınıfı aradan çıkartılmalı:

@Override public boolean equals(Object o) {
       return o instanceof CaseInsensitiveString &&
           ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);



Geçişli
Bu kriteri kırmak da çok zor değil. Point sınıfından türeyen bir ColorPoint sınıfında koordinatlara ek olarak getirilen renk değişkeni equals metoduna eklenince olanlar oluyor:

public class Point {
       private final int x;
       private final int y;
       public Point(int x, int y) {
this.x = x;
this.y = y; }
       @Override public boolean equals(Object o) {
           if (!(o instanceof Point))
               return false;
           Point p = (Point)o;
           return p.x == x && p.y == y;
}
       ...  // Remainder omitted
}

public class ColorPoint extends Point {
       private final Color color;
       public ColorPoint(int x, int y, Color color) {
           super(x, y);
           this.color = color;
       }
       ...  // Remainder omitted
   }
// Broken - violates transitivity!
   @Override public boolean equals(Object o) {
       if (!(o instanceof Point))
           return false;
       // If o is a normal Point, do a color-blind comparison
       if (!(o instanceof ColorPoint))
           return o.equals(this);
       // o is a ColorPoint; do a full comparison
       return super.equals(o) && ((ColorPoint)o).color == color;
   }

  
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1 ile p2, p2 ile p3 birbirlerine eşit iken p1 ile p3 birbirine eşit olmalıyken maalesef eşitlik kontrolü bize false değerini dönecektir. Java kütüphanesinde bu kriteri kıran sınıflar bulmak mümkün. Örneğin Date ve Timestamp sınıfları. Ayrıntılı değerlendirmeyi "Effective Java" kitabından edinebilirsiniz. 

Bu sorun için çözüm ise gene bir Object Oriented tasarım ilkesini adres gösteriyor. "is-a ilişkisi yerine has-a" ilişkisi kullanmak. Point sınıfından ColorPoint sınıfı türetmek yerine ColorPoint sınıfı içerisinde Point sınıfının referansını barındırsın.


Tutarlılık
Eşitlik kontrolü yapılacak değişkenler her zaman tutarlı değerlere sahip olmalıdır. Örneğin java.net.URL sınıfının eşitlik kontrolü IP adresi ile ilişkilendirilen host name değişkeni üzerinden yapılıyor. Bu yüzden ağ erişiminin yanı sıra host name ile ilişkilendirilmiş IP adresi de değişebilir. Sağlıklı bir karşılaştırma her zaman yapılamayacaktır.


Equals metodu için bu olmazsa olmazların yanında sorguyu hızlandıracak ve kesirli değerler için equals yerine Float.compare() ya da Double.compare() kullanmanız gerektiğini söyleyen bir çok minik ipucunu "Effective Java" kitabında bulabilirsiniz. Kitabın Java ile geliştirim yaparken dikkat edilmesini önerdiği diğer konular da her Java geliştiricisi tarafından sindirilip kod yazarken uygulanmalı. Kitaptan bir tane edinin derim :)



25.11.2011

JSF ve Spring Security ile Önceden Kimlik Kontrolü Yapılmış Kullanıcı için Login Sayfasının Tekrar Gösterilmemesi

Başlık biraz uzun oldu ama anlatacağım konuyu başlıkta ancak bu kadar özetleyebildim. Özetin açıklamasını yapmaya çalışırsak: web uygulamasına giriş yapmış, yani kimliğini doğrulatmış bir kullanıcı, belirli sayfalara istekte bulunduğunda (örneğin login sayfası için tekrar istek gönderirse) zaten yetkilendirildiğini belirleyip isteği başka bir sayfaya yönlendirmek (örneğin login sayfasından sonra gösterilecek index sayfasına) istiyoruz.

Spring Security'de bu işin kolay yolu var mıdır diye küçük bir araştırma yaptık ama ya doğru kelimeleri yine bulamadık ki blog başlığı seçerken de yaşadım aynı sıkıntıyı ( :) ) ya da diğerler geliştiriciler böyle bir gereksinime ihtiyaç duymamışlar. İlki seçenek akla daha yatkın..

Çalışma arkadaşım Basri Kahveci'nin önerdiği ve bir projede uyguladığı yöntem de aklıma yatmayınca (Çalışıyor ama biraz kulağı tersten tutmak gibiydi :)) Küçük bir "Nasıl yapabiliriz?" düşünmesinin ardından aslında basit bir JSF PhaseListener kullanarak yapılabileceğini farkettim.

Bildiğiniz üzere JSF yaşam döngüsü temel olarak 6 adımdan oluşur (Yaşam döngüsünü detayıyla incelemek isterseniz BalusC bu konuda emek harcamış):




Kısaca hatırlamak gerekirse Request parametresi barındıran bir istek bu 6 adımdan geçerek kullanıcıya hedef sayfa döndürülür. Bir sayfaya yapılan ilk istek, yani parametre barındırmayan bir istek ise ilk adımdan sonra yaşam döngüsünü dolaşmak yerine Render Response adımına yönlendirilip kullanıcının istediği sayfa gösterilir. Her adımda gerçekleşen işlemleri isterseniz başka bir blog girdisinde bahsederiz.

JSF bize bu adımların arasına girebilmek için PhaseListener interface'ini kullanıma sunuyor. Görüldüğü gibi PhaseListener'da 3 metot mevcut. İkisi yaşam döngüsü adımlarının herbirine girerken ve herbirinden çıkarken işletilecek kodları barındırırken, üçüncü metot da hangi adımda bu listener'ın çalıştırılması gerektiğini belirtmemize olanak sağlıyor. Bu metotta PhaseId.ANY_PHASE gibi bir değer döndürerek her bir adım için listenerın tetiklenmesini sağlayabileceğimiz gibi PhaseId.RENDER_RESPONSE benzeri bir değerle belirli bir adımda tetiklenmesini de sağlayabiliyoruz.

Bizim senaryomuzda da login sayfamız render olmadan önce bir PhaseListenerın tetiklenmesini sağlayarak, kullanıcının önceden sisteme giriş yapıp yapmadığını belirleyip gerekiyorsa başka bir sayfaya yönlendirme işlemini yapmak mümkün:

public class AuthPhaseListener implements PhaseListener {

 @Override
 public void afterPhase(PhaseEvent event) {
 }

 @Override
 public void beforePhase(PhaseEvent event) {
  User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  if(user != null){
   FacesContext.getCurrentInstance().getExternalContext().redirect("main");
  }

 }

 @Override
 public PhaseId getPhaseId() {
  return PhaseId.RENDER_RESPONSE;
 }

}

beforePhase() metodu içinde Spring Security'nin SecurityContextHolder'ı ile sessionda kayıtlı bir kullanıcı  olup olmadığını kontrol ettik. Eğer bir kullanıcı nesnesi elde edebiliyorsak FacesContext ile de basitçe yönlendirme işlemini yapıyoruz.

Geriye yazdığımız bu listener sınıfını projemize entegre etmeye geldi. Eğer listener tanımını faces-config.xml içerisinde yaparsak her sayfa için işletilecek yaşam döngüsünde sınıfımız tetiklenecektir. Bizim istediğimiz sadece login sayfasında bunu tetiklemek. O yüzden tanımı listenerı kullanmak istediğimiz sayfa bazında yapmamız gerekli. JSF sayfamıza şu tag'i eklemek yeterli:

<f:phaseListener type="com.prime.dasgin.AuthPhaseListener"/>


JSF kullanılmayan projelerde benzer bir mantık web.xml'de tanımlanan Servlet Filter'lar ile de gerçekleştirmek mümkündür. Login sayfasına yapılacak olan bir istekte devreye girecek bir Filter tanımlamak ve benzer bir süreci içinde işletmek sorunu çözer.



16.10.2011

MySQL'in String karşılaştırması

Var olan bir projeye sonradan katıldıysanız ya da proje ekibinizde veritabanı işlerini yöneten bir takım arkadaşınız varsa MySQL'in string karşılaştırmasının nasıl çalıştığı ile ilgili bilginiz olmayabilir. Ve vakit gelip de yeni bir projenin veritabanını oluşturma ihtiyacı duyduğunuzda ve iş başa düştüyse MySQL karakter encoding ve string karşılaştırmayla ilgili anlatacağım bu konu size zaman kazandıracaktır.

MySQL'de nonbinary string tipleri (char, varchar, text) büyük küçük harf ayırmaksızın karşılaştırılır. Örneğin username like 'admin' gibi bir sorgu koşulunda dönen sonuçlar içinde username'i admin, ADmin, ADMIN, vb. olan kayıtlar da getirilecektir.

Böyle bir sonucu engellemek için ya veritabanı oluşturulurken schema tanımının collation tipini ya da sorunu yaşadığınız kolonun collation tipini binary olacak şekilde ayarlamanız gerekiyor:

CREATE DATABASE mobileme CHARACTER SET utf8 COLLATE utf8_bin;

ya da

ALTER TABLE table_name MODIFY username VARCHAR(25) CHARACTER SET utf8 COLLATE utf8_bin;


Varolan schema'da böyle bir sıkıntınız varsa tüm schema'nın collate tipini değiştirmek için aşağıdaki sorgunun ürettiği sorgucukları işletmeniz yeterli:

SELECT CONCAT('ALTER TABLE `', t.`TABLE_SCHEMA`, '`.`', t.`TABLE_NAME`, '` CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;') as stmt
FROM `information_schema`.`TABLES` t
WHERE 1
AND t.`TABLE_SCHEMA` = 'schema_name'
ORDER BY 1;

25.04.2011

Türkçe Maven Rehberi

"Kahvenin Hatırı" adlı blogta Sonatype'ın Maven belgelerinin Türkçe'ye çevrilmiş hallerine ulaşabilirsiniz. Son blog girdilerinde Maven ile ilgili örnekler de mevcut. Emeği geçen arkadaşa teşekkür ederiz:

http://kahveninhatiri.blogspot.com/2011/04/turkce-maven-rehberi.html

17.04.2011

JSF 2.0 ve Composite Components

JSF 1.2 ve Facelets ile halihazırdaki bileşenleri biraraya getirip yeni bir tag yaratarak tek bir bileşen olarak kullanabiliyorduk. Bunun için izlenen adımlar:

1) Biraraya getirilecek bileşenlerin .xhtml dosyasında <ui:composition> tagi altında yazılması.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:h="http://java.sun.com/jsf/html">
  
 <ui:composition>
  <h:outputLabel value="#{label}" for="#{id}"/>
  <h:inputText id="#{id}" value="#{value}"/>
 </ui:composition>
  
</html>


2) Hazırlanan bu .xhtml dosyasının namespace'i belirtilmiş *.taglib.xml içerisinde ismi ve dosya adresini belirten bir tag olarak tanımlanması.

<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib xmlns="http://java.sun.com/xml/ns/javaee"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
                version="2.0">

    <namespace>http://mdasgin.blogspot.com/custom</namespace>
    
    <tag>
     <tag-name>myInput</tag-name>
     <source>tags/customTag.xhtml</source>
    </tag>
</facelet-taglib>


3) *.taglib.xml' in web.xml'de tanımının yapılması.

<context-param>
 <param-name>javax.faces.FACELETS_LIBRARIES</param-name>
 <param-value>
        /WEB-INF/taglibs/custom.taglib.xml;
    </param-value>
</context-param>

4) Yeni yaratılan tag'in kullanılmak istendiği yerde namespace ve tag ismi belirtilerek kullanılmasını içermekteydi.

<html xmlns:m="http://mdasgin.blogspot.com/custom">
..
<m:myInput id="myInput" label="Username" value="#{user.name}"/>
..
</html>



Ne var ki bu şekilde Facelets ile yapılan yeni bileşen yaratma işleminde yaratılan bileşene sadece Value Expression tanımları geçilebiliyordu. Örneğin h:inputText'in yanına h:commandButton bileşeni ekleyerek commandButton'un kullanımına <m:myInput action="#{user.save}"> gibi bir Method Expression parametresi veremiyorduk.

Neyseki JSF 2.0 ile gelen Composite Components özelliği ile geliştiriciyi yoran gereksiz adımlardan ve yeni bileşenimize method expression aktaramama gibi kısıtlardan kurtuluyoruz. WEB-INF/resources klasörü içinde namespace görevi gören bir dizin oluşturmak ve bu dizin altında bileşenIsmi.xhtml dosyası hazırlamamız yeterli:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:composite="http://java.sun.com/jsf/composite"
   xmlns:h="http://java.sun.com/jsf/html"
   xmlns:p="http://primefaces.prime.com.tr/ui">
 
 <composite:interface>
  <composite:attribute name="id" required="true"/>
  <composite:attribute name="label" required="true"/>
  <composite:attribute name="value"/>
  <composite:attribute name="action" method-signature="void action()"/>
 </composite:interface>
 
 <composite:implementation>
  <h:outputLabel value="#{cc.attrs.label}" for="#{cc.attrs.id}"/>
  <h:inputText id="#{cc.attrs.id}" value="${cc.attrs.value}"/>
  <h:commandButton value="Save" action="#{cc.attrs.action}"/> 
 </composite:implementation>
 
</html>


Yukarda /WEB-INF/resources/components dizini altına myInput.xhtml dosyasının içeriği mevcut. Ayrıntılara  göz atarsak, JSF 2.0 ile gelen composite namespace'inin kullanımı hemen dikkat çekiyor. Dosya içeriği composite:interface ve composite:implementation olarak ikiye ayrılıyor. Implementation kısmında istenen yetenek gerçekleştirilirken interface kısmında yeni bileşenin ihtiyaç duyduğu değişkenler sıralanıyor. Değişkenleri detaylı incelersek value expression alan değişkenlerin yanında method expression alan action değişkeninin tanımlanması örneklemiştir. Ayrıca değişkenlere erişim de görüldüğü gibi #{cc.attrs.degiskenIsmi}  şeklinde yapılıyor.

Yeni bileşenimizi bu şekilde hazırladıktan sonra hiçbir ek tanım yapmadan JSF sayfalarımızda kullanabiliriz:

<html xmlns:m="http://java.sun.com/jsf/composite/components">
 ...
  <m:myInput id="myInput" label="Username" value="#{user.name}" action="${user.save}"/>
 ...
</html>

Küçük bir not: Composite Components kullanırken tanımladığınız değişkenin ismi "rendered" olursa JSF'in hata fırlattığını da hatırlatayım. İlgili bug kaydı burada.

17.03.2011

JSF2'de Flash Scope Sorunu

Redirect işlemlerinde sunucuya ikinci bir istem gönderildiği için Request Scope içindeki değişkenler kayboluyor, bu yüzden aktarılacak değişkenler Session Scope'ta tutuluyordu. Session scope'un gereksiz meşgul edilmemesi için JSF2 ile birlikte Map gerçekleştirimi olan flash nesnesi kullanıma sunuldu. Böylelikle redirect işlemlerinde değişken aktarımı ve kullanımı Java sınıfı içerisinde:

ExternalContext.getFlash().put("message", "Your password is about to expire");
JSF sayfası içerisinde de:
#{flash.message}
şeklinde yapılabilir hale geldi. Ne var ki bu özellik Mojarra 2.0.3 impl'de (MyFaces ile denemedim) sadece aynı dizin ve altdizinlerde bulunan sayfalara redirect yapılırsa çalışıyor. Farklı dizin yapılarında bulunan sayfalar arasında yapılan redirect işleminde bir bug nedeniyle Flash scope'a konulan değişkene ulaşamıyorsunuz. Sorunun nerden kaynaklandığı ile ilgili bir bağlantı şurda.. Böyle bir gereksinimde gene eski usül, Session Scope'tan faydalanmak ve ilgili değişken redirect yapılan sayfada kullanıldıktan sonra Session Scope'tan değişkeni kaldırmayı unutmamak gerekiyor.

16.02.2011

JSF'de SMTPAppender ile Gmail üzerinden Exception Bildirimi

Geliştirdiğimiz uygulamaların kullanımı esnasında oluşabilecek kullanıcı kaynaklı ya da sistemsel hataları yakalayıp kullanıcıyı bilgilendirmek ya da yönlendirmek için yoğun çaba harcarız. Harcanan çabaya rağmen gözden kaçırılan bir hatanın oluşması durumunda ise hatanın sebebini bulmak için genelde loglar kurcalanır. Bir hata oluştuğunu anlamak için de ya kullanıcı geri bildirimi ya da rutin log kontrolleri yapılır. Bu geri bildirimin hata oluştuğu esnada bir ileti ile otomatik olarak yapılması da hataya müdahale için iyi bir yöntemdir. Bunun için popüler loglama kütüphanesi log4j'nin SMTPAppender sınıfı kullanılabilir. Örneğin yakalanan bir Exception'ın error seviyesinde loglanması ile log4j'nin bir ileti göndermesi sağlanabilir.

Bu iş için Gmail'i kullanabilir miyiz diye küçük bir arama yaptığımda şu blog girdisine rastladım. Örnek kodta Gmail ayarları barındıran bir Enum mevcut. Mail gönderimi için de Spring çatısının Email desteği kullanılmış. Spring Email kullanabilmek için ayrıca activation.jar ve mail.jar kütüphaneleri de gerekli. Madem Spring kullanacağız bağımlılık yönetimini Spring'e bırakalım. Enum sınıfı yerine öncelikle JavaMailSenderImpl yaratan bir bean tanımı yapalım:


<bean id="javaMailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
        <property name="host" value="smtp.googlemail.com"/>
        <property name="port" value="587"/>
        <property name="username" value="gmailAccount@gmail.com"/>
        <property name="password" value="armut123"/>
        <property name="javaMailProperties">
            <props>
                <prop key="mail.smtp.auth">true</prop>                
                <prop key="mail.smtp.starttls.enable">true</prop>                
            </props>
        </property>
    </bean>

Yapmış olduğumuz bean tanımını ApplicationContext'ten alıp GMailAppender sınıfının kullanımına sunacak olan JavaMailSenderFactory sınıfını yazalım:

@Component
public class JavaMailSenderFactory implements ApplicationContextAware {

    private static ApplicationContext applicationContext;
    
    public static JavaMailSender createJavaMailSender(){
        return applicationContext.getBean("javaMailSender", JavaMailSender.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

JavaMailSenderFactory sınıfını da annotation yardımıyla Spring bean olarak tanımladık. Bean tanımını XML ile de yapabilirdik. Bu sınıf ApplicationContextAware arayüzünü gerçekleştirdiği için Spring application context'e erişerek JavaMailSender beanine ulaşabiliriz. Bunun için bean tanımını döndüren static bir metot yeterli olur. Log4j'nin SMTPAppender sınıfınından türetilen GMailAppender sınıfının bizim yaptığımız değişiklikler sonrasında aldığı hal şu şekilde:

public class GMailAppender extends SMTPAppender {
    private static final String NL = System.getProperty("line.separator");
    
    @Override
    public void append(LoggingEvent event){
        
        SimpleMailMessage msg = new SimpleMailMessage();
        StringBuilder builder = new StringBuilder();
        builder.append(getLayout().format(event));
        builder.append(event.getMessage().toString());
        if (event.getThrowableInformation() != null) {
            builder.append(NL);
            String[] stackTrace = event.getThrowableInformation().getThrowableStrRep();
            for(int i = 0; i < stackTrace.length; i++) {
                builder.append(stackTrace[i] + NL);
            }
        }
        String[] senders = getTo().trim().replace(" ", "").split(",");
        
        msg.setTo(senders);
        msg.setText(builder.toString());
        msg.setSubject(this.getSubject());
        
        try{
            JavaMailSender javaMail = JavaMailSenderFactory.createJavaMailSender();
            javaMail.send(msg);
        }catch (MailException e) {
            System.err.println("!!! HATA MAILI GONDERILEMEDI !!! : ");
            e.printStackTrace();
        }
    }
}

Referans aldığım sitedeki append()metodunu da az biraz değiştirdim. Değişikliğin nedeni append() metodunun super.append(event) ile akışı SMTPAppender sınıfına aktarmış olması. GMailAppender içinde gerekli biçimlendirme ve gönderim işlemi yapıldığı için buna gerek yok aslında. Şimdi de GMailAppender için Log4J ayarlarını yapılandıralım. Ben properties dosyası yerine log4j.xml kullanmayı tercih ettim:

<root>
        <priority value="info" />
        <appender-ref ref="GMailAppender" />
    </root>

    <appender name="GMailAppender" class="com.prime.mobileme.core.util.appender.GMailAppender">
        <param name="BufferSize" value="1" />
        <param name="SMTPDebug" value="false" />
        <param name="To" value="dasgin@gmail.com" />
        <param name="Subject" value="Ucundan Java ERROR" />
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern"    value="[%d] [%t] %-5p %c %x" />
        </layout>
        <filter class="org.apache.log4j.varia.LevelRangeFilter">
            <param name="LevelMin" value="error" />
            <param name="LevelMax" value="fatal" />
        </filter>
    </appender>


Yukarıdaki ayarlardan da anlaşılacağı gibi yakaladığınız Exception'ları log4j ile error ya da fatal olarak loglarsanız loglanan hata mail olarak belirttiğiniz Gmail hesabı üzerinden gönderilecektir. Peki yakalayamadığımız hata durumlarında ne yapmalı? Eğer JSF 2 kullanıyorsanız şu blog girdisinden faydalanabiliriz. Gene buradaki örneği kendi ihtiyaçlarımıza göre yeniden düzenleyelim. Öncelikle oluşan hatayı ele alarak log4j'ye error olarak log düşen ExceptionHandler sınıfını hazırlayalım:

public class MyExceptionHandler extends ExceptionHandlerWrapper {

    private static Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);
    private ExceptionHandler wrappedExceptionHandler;
    
    public MyExceptionHandler(ExceptionHandler exceptionHandler){
        this.wrappedExceptionHandler = exceptionHandler;
    }
    
    @Override
    public ExceptionHandler getWrapped() {
        return wrappedExceptionHandler;
    }
    
    @Override
    public void handle(){
        
        for (ExceptionQueuedEvent event : getUnhandledExceptionQueuedEvents()) {
            ExceptionQueuedEventContext context = (ExceptionQueuedEventContext)event.getSource();
            Throwable t = context.getException();
            logger.error("Unhandled Exception: ", t);
        }
        getWrapped().handle();

    }

}

Ardından ExceptionHandler sınıfımızı JSF'in kullanabilmesi için ExceptionHandlerFactory sınımızı hazırlayalım:

public class MyExceptionHandlerFactory extends ExceptionHandlerFactory {

    private ExceptionHandlerFactory parent;
    
    public MyExceptionHandlerFactory(ExceptionHandlerFactory parent){
        this.parent = parent;
    }
    
    @Override
    public ExceptionHandler getExceptionHandler() {
        return new MyExceptionHandler(parent.getExceptionHandler());
    }

}

En son adımda da faces-config.xml'e yazdığımız factory sınıfını tanıtmak kalıyor:

<factory>
          <exception-handler-factory>
            ucundan.java.MyExceptionHandlerFactory
          </exception-handler-factory>
    </factory>


Kaynaklar:
GMail and log4j e-mail appender - error STARTTLS
JSF 2 Exception Handling