pátek 9. září 2005

Java – znakové kódování

Jedno z nejčastěji tázaných témat, v diskusních skupinách věnovaných Jave, se točí kolem kódování znaků. Nejčastěji se to týká kódování při čtení/zápisu servletu (JSP), databáze a souborů. Obecně to může generalizovat na operace probíhající na hranici mezi JVM a okolním světem, kde dochází převodu bytové reprezentace na znakovou reprezentaci a naopak. Mě osobně nejvíce problematiku kódování v Jave osvětlil Makub v diskusní skupině na serveru java.cz a tak se pokusím podělit.

Nemám problém protože všechno je Unicode

Výše uvedený nadpis si najdete v kdejakém pamfletu o Jave. Ano je to skutečně tak, že Java pracuje vnitřně se všemi znaky jako s Unicode. Unicode přiřazuje každému znaku jedinečné číslo a definuje tři formy, jak může být toto číslo reprezentováno v byte, word nebo double word formě (8, 16, 32).

To že Java pracuje vnitřně s Unicode (používá UTF-16), je tak omlácenou frází, že vede mnoho vývojářů k přesvědčení, že se nemusí o kódování starat. Opak je pravdou. Při jakékoliv probíhající na hranici mezi JVM a okolním světem, například čtení textového souboru nebo zápis do databáze, dochází k převodu z vnitřní unicodovské reprezentace znaku na jeho bytovou reprezentaci a naopak.

Vnější bytová reprezentace pak odpovídá zvolenému kódování. Pokud toto kódování neurčíme, použije JVM defaultní kódování, které závisí na prostředí, v němž běží. Právě při převodu znaku na bajtovou vnější reprezentace a naopak dochází k problémům. Pokud se defaultní kódování JVM liší s kódováním, které chceme na vstupu/výstupu a my toto kódování explicitně Jave neurčíme, dojde k převodu na bytovou reprezentaci či naopak, podle defaultního kódování.

Klasickým příkladem je zpracování HTTP požadavku v servletu. Pokud začneme číst parametry HTTP požadavku, před tím než zavoláme metodu setCharacterEncoding, dojde k tomu, že servletový kontejner použije defaultní kódování pro všechny parametry.

Jednoduchá rada, plynoucí z výše uvedeného, tedy zní:Explicitně určujte kódování pro všechny vstupně/výstupní operace mimo JVM. Problém této rady je v tom, že vývojáři si často neuvědomí co je mimo JVM. Takže jedná se například o práci s databází, souborovým systémem nebo zpracování HTTP požadavku.

Na závěr jsem si nechal pár praktických rad, které řeší 90% problémů.

Obecné - žádné texty ve zdrojových kódech

Vyčleňte všechny aplikační texty do resource bundle a ušetříte si s tím plno problému.

Obecné – čtení/zápis textových zdrojů

Používejte třídy java.io.InputStreamReader a java.io.OutputStreamWriter.

Obecné - nastavte kódování pro kompiler

Pokud používáte ve zdrojovém kódu aplikační texty např. pro výjimky, nezapomeňte určit kompileru kódování. Slouží pro to parametr encoding.

Obecné - zkonvertujte properties resource bundle před použitím

Pokud používáte resource bundle založený na properties souboru, prožeňte jej, v rámci build procesu, přes utilitku native2ascii. Ta je standardní součástí Java SDK.

 
Antovský task:
<target name="native2ascii"> 
    <native2ascii 
 encoding="utf-8" 
 src="${src.dir}" 
 dest="${web.dir}/WEB-INF/classes" 
 includes="**/*.properties" 
 ext=".properties"/> 
</target>
 
Server - nastavte kódování pro HTTP request/response

Před tím než přečtete první parametr a zapíšete první znak na výstup nastavte pro oba požadované kódování. V případě HTTP požadavku se k tomu dá elegantně využít servletový filtr. Filtrem zamezíte případným problémům, které mohou nastat v případě, že si na parametry "šáhne" někdo před vámi, např. validátor vstupních parametrů.

 
public class RequestEncodindFilter implements Filter{    
  public void doFilter(
  ServletRequest rq, 
  ServletResponse re, 
  FilterChain chain) 
    throws IOException, ServletException {

            rq.setCharacterEncoding("utf-8");//kodovani          
            chain.doFilter(rq,re);  
  } 
 
  public void init(FilterConfig fc) throws ServletException {}
  public void destroy() {}
}

web.xml

<filter>  
  <filter-name>RequestEncodindFilter</filter-name>
  

<filter-class>filterpackage.RequestEncodindFilter</filter-class> 
</filter>
 
<filter-mapping>
  <filter-name>RequestEncodindFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
 

Pro JSP stránky používejte direktivu page a atribut pageEncoding a contentType

 
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
 
Databáze - nastavte kódování pro JDBC ovladač

JTDS ovladače mají parametr, kterým určíte kódování pro práci s CHAR/VARCHAR/TEXT typy.

 
Connection string MySQL JDBC ovladače:
jdbc:mysql://localhost/devdb1?useUnicode=true&characterEncoding=UTF-8