Servlet, JSP and UTF-8 best practices

Paluu utf8 sivulle
Alkuperäinen vastaukseni muropaketissa

This page, in finnish, describes how servlet, JSP, Mysql database and UTF-8 work together.

Tässä temput jotka otettava huomioon UTF-8 tuen kanssa. Unicode vaatii tarkkuutta MySql, sovelluspalvelin ja clienttiin lähetettävän html sivujen kanssa. Itse olen aikoinaan kivuliaasti opiskellut mitä tarkalleen tapahtuu eri tasoilla, jotta saan mahdolliset ongelmat korjattua. Tässä lyhyt oppimäärä.

MySQL tietokannan perustaminen

Ei luoteta serverin ja tietokannan oletus charsettiin, vaan kerrotaan se taulukohtaisesti. Älä luota oletuksiin, vaikka itse olisit luonut tietokannat.
-- perusta tietokanta utf-8 oletuksella
CREATE DATABASE mydb DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_swedish_ci;

-- perusta taulu utf-8 oletuksella
CREATE TABLE tMyTable (
  id int(11) NOT NULL auto_increment,
  code VARCHAR(20) NOT NULL,
  name VARCHAR(20) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_swedish_ci;

JDBC tietokantaurlin määritys

Mysql ajurin (com.mysql.jdbc.Driver) urliin määritetään unicoden käyttö, jotta varmasti päristin tietää mitä tekee.
jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8

Tomcatin mywebapp/META-INF/context.xml tiedostoon jdbcpool määritys

Määritetään jdbcpool resurssin käyttö webapplikaation context.xml tiedostoon. Voit optimoida min-max yhteyksien määrää. Yleensä ei kovin montaa yhtäaikaista tarvitse koska DAO-luokat eivät pidä sqlconnection oliota kovin kauan varattuna.
<Resource name="jdbc/mydb" auth="Container" type="javax.sql.DataSource"
  maxActive="10" maxIdle="2" maxWait="10000"
  username="myuid" password="mypwd"
  driverClassName="com.mysql.jdbc.Driver"
  url="jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8"
  validationQuery="SELECT 1"
/>

Tähän asti kaikkia on vielä helppoa, mutta sitten päästäänkin hauskaan osuuteen eli sovelluspalvelimen ja html-selaimen yhteistyöhön. Siihen liittyy paljon salatiedettä, jos ei ole asioihin tarkkaan perehtynyt.

Tomcat sovelluspalvelimet T4, T5 ja T6

Alkusanoina mainittakoon Tomcat4.x ja Tomcat5.x toimivat eri tavalla GET moodin postauksessa mitä tulee charsetin käyttöön. Tomcat4.x versiossa GET ja POST mooodin url-parametrit muutetaan tavukoodista merkkijonoksi requestista löytyvällä charset arvolla. Uudemmassa Tomcat5.x lähtien oletuksena requestin charset arvo käytetään vain POST moodin tiedoille ja GET moodin parametrit muutetaan ISO-8859-1 charsetilla.

Requestin charset tulee joko requestin otsikkokentässä Content-Type: text/plain; charset=UTF-8 tai servletin alussa asetetaan manuaalisesti request.setCharacterEncoding("UTF-8") metodilla.

Mutta T5 tapauksessa request.setCharacterEncoding metodi vaikuttaa vain post moodin formikenttiin eikä get moodissa tulevin joihin se käyttää aina ISO-8859-1 merkistöä.

Enabloitava connectoriin useBodyEncodingForURI parametri conf/server.xml tiedostossa, jos haluaa muuttaa T5/T6 version toiminnan samanlaiseksi kuin T4 versiossa. Tätä ei tarvitse tehdä, jos htm-sivun formit lähetetään aina post moodissa. Muutoksen jälkeen setCharacterEncoding metodi vaikuttaa sekä post ja get moodin parametritietoihin. Itse lisään parametrin kaikkiin Tomcat asennuksiin.
    <!-- non-SSL Coyote HTTP/1.1 Connector -->
    <Connector port="8080"
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" redirectPort="8443" acceptCount="100"
               debug="0" connectionTimeout="20000"
               disableUploadTimeout="true"
               useBodyEncodingForURI="true"
    />

Servletin ja .jsp sivun alussa kerrotaan charset

Servletin alussa asetetaan charset, koska se yleensä puuttuu requestin otsikkotiedoista. Nykyiset selaimet ovat bugisia ja eivät kerro charset attribuutilla missä muodossa parametrit lähetettiin selaimesta serveriin. Suckers.

Servletin alussa asetetaan charset, jota selaimen postaus (get tai post) on käyttänyt. Se on kerrottava ennen kuin luet mitään tietoja requestista, tai saattaa olla jo myöhäistä.

Tässä on huomioitava mahdolliset filtterit, jotka esiprosessoivat requestia ennen omaa servlettikoodia. Silloin se on jo myöhäistä asettaa täällä ja saat parametrien arvot väärin. Charset on asetettava filtterin alussa, jotta toimisi oikein koko putken läpi.
public void doGet(HttpServletRequest req, HttpServletResponse res)
      throws ServletException {
   doPost(req, res);
}

public void doPost(HttpServletRequest req, HttpServletResponse res)
      throws ServletException {
   // set utf-8 charset
   if (req.getCharacterEncoding() == null)
      req.setCharacterEncoding("UTF-8");

   String value = request.getParameter("fieldName");
   ...
}
Sitten .jsp sivujen kanssa on oltava erityisen tarkkana mitä tekee, koska viattomat välilyönnit ja rivinvaihdot saattava pilata hyvän yrityksen. Alla esimerkki miten .jsp sivun otsikkotiedot pitää kirjoittaa, huomaa miten turhat whitespace merkit poistettu alusta. Blokkien <%.... ja ....%> lopetusmerkit ovat peräkkäin ilman rivinvaihtoa, jolloin ei whitespace merkkejä generoidu liian aikaisin.

contentType arvo laitetaan responsen header tietoihin ja pageEncoding kertoo kääntäjälle .jsp tekstitiedoston levypinnan talletusformaatin.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="ISO-8859-1"
  import="java.util.*, java.io.*"
%><%
   request.setCharacterEncoding("UTF-8");
   String myvalue = "hello all and ÅÄÖ";
   String param = request.getParameter("fieldName");
   myvalue += " " + param;
%>
<html>
<head>
  <title>Page Title</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="keywords" content="some,fine,keywords" />
</head>
<body>
your html content goes here ja sama suomeksi ABCÅÄÖ.... <%= myvalue %> 
</body>
</html>

Servlet taglib kirjaston käyttö .jsp sivussa

Jos käytät taglib kirjastoja .jsp sivulla, kirjoita sivun alkuun tiedot seuraavasti. Huomaa miten viattomat whitespace merkit vältetään kirjoittamalla <%|%> tagit tarkasti ilman turha välimerkkejä.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ 
    taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml"  %><%@ 
    page contentType="text/html; charset=UTF-8" pageEncoding="ISO-8859-1"
    import="java.util.*,
             java.io.*
	"
%><%
   request.setCharacterEncoding("UTF-8");
   String myvalue = "hello all and ÅÄÖ";
   String param = request.getParameter("fieldName");
   myvalue += " " + param;
%>
<html>
<head>
  <title>Page Title</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <meta name="keywords" content="some,fine,keywords" />
</head>
<body>
your html content goes here.... <%= myvalue %>
</body>
</html>

XML dokumentin generointi .jsp sivulla

XML-dokumentin generointi jsp-sivusta näyttäisi tältä. Huomaa miten xml dokumentin otsikkorivi on samalla rivilla %> merkin kanssa jotta ei tule "viatonta" whitespace merkkiä alkuun. XML parserit ovat tarkkoja ettei otsikkorivin alussa ole ylimääräistä merkkiä.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ 
    page contentType="text/xml; charset=UTF-8" pageEncoding="ISO-8859-1"
    import="java.util.*, 
             java.io.*
	"
%><%
  // MyBean has getId() and getName() getters
  List<MyBean> items = new ArrayList<MyBean>();
  items.add( new MyBean(1, "first") );
  items.add( new MyBean(2, "second") );
  items.add( new MyBean(3, "third") );

  // publish variables to JSTL scope
  pageContext.setAttribute("items", items);
%><?xml version="1.0" encoding="UTF-8"?>
<mydoc>
<c:forEach var="item" items="${items}">
  <item>
    <id>${item.id}</id>
    <name>${item.name}</name>
  </item>
</c:forEach>
</mydoc>

Selainten charset kikkailu

Jep, eli selaimet ovat bugisia tästä oletustilanteesta on valitettavasti aina lähdettävä liikkeelle. Ne eivät kerro postauksen (get tai post) otsikkotiedoissa mitä charsettia url- tai formiparametrien bytesToString konversiossa olisi käytettävä. Sinun kooderina on se vain tiedettävä sivukohtaisesti.

Jos html-sivun responsen headeriarvosta löytyy Content-Type: text/html; charset=UTF-8 tai <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> sivun alussa kertoo charsetin, tulee selain käyttämään mainittua charset arvoa formin kenttien postaukseen.

Selaimet käyttävät samaa charsettia sekä POST ja GET moodin formipostaukseen, tämän takia suosittelen Tomcat5/6 serverin konfigurointiin aiemmin mainittua useBodyEncodingForURI="true" lisäystä.

Mutta jos kirjoitat käsin osoitekentän urlin, selain saattaa lähettää osoitteen ISO-8859-1 charsetilla. Joudut käsin kirjoittamaan valmiiksi eskapoidun urlin tai selaimen asetuksista laittamaan oletuscharsetin utf8 tilaan.

ISO-8859-1 eskapoitu "ABC ÅÄÖ" tieto: ?key=ABC%20%C5%C4%D6
UTF-8 eskapoitu "ABC ÅÄÖ" tieto: ?key=ABC%20%C3%85%C3%84%C3%96

Huom! välilyönnin voit eskapoida joko + merkillä tai %20 hexaeskapoinnilla.

Huom! tämän selainten ominaisuuden takia Tomcat5.x versioissa request.setCharacterEncoding("UTF-8") metodi oletuksena vaikuttaa vain POST moodin kenttiin eikä GET moodiin. Se olettaa, että get moodin tiedot ovat "käsin syötettyjä" tietoja ja niille on parempi käyttää ISO-8859-1 charsettia. Minun mielestäni Tomcat5/6 olettavat väärin, nyt pitäisi jo olla kaikki sovellukset unicode kansalaisia.

Tämän yksityiskohdan takia itse suosittelen aiemmin mainittua useBodyEncodingForURI käyttöä Tomcat connectorien server.xml asetuksissa. Sitten html-sivujen formien get tai post moodi toimivat oikein. Käsin syötetty urli toimivat kunhan siihen syöttää turvallisia us-ascii merkkejä tai osaa eskapoida valmiiksi utf-8 muodossa merkit.

Itselleni nämä ovat jo helppoa kauraa ja olen vuosia tehnyt sovellukset UTF-8 enabloituna clientista tietokantaan saakka. Ei tarvitse välittää vaikka mitä viidakkomerkkejä käyttäjät syöttävät kenttiin ja aina toimii. Kaikki sovelluksen konffifilutkin talletan UTF-8 formaatissa.


edit 1 (Hezauruksen kommentti):
mysql kannan collation määrityksen tarkennus. utf8_general_ci määritys sorttaa ääkköset sekä isot/pienet kirjaimet väärin. Se on globaalin sovelluksen kompromissi jos ei muuta kollaatiota voi käyttää. Käyttämällä utf8_swedish_ci asetusta toimivat mysql lajittelut meidän näkökulmasta oikein.
edit 2 (Tronic kommentti):
Osa selaimista osaa lähettää osoitekenttään suoraan kirjoitetut merkit valmiiksi utf-8 eskapoituna, tai pystyy erikseen asettamaan päälle selaimesta.