Categoria: Arquitetura de Software (Página 2 de 2)

Instalando e configurando o JDK 7 no linux Ubuntu

Neste pequeno tutorial você aprenderá a instalar o Java Development Kit 7 no linux Ubuntu. O JDK consiste no conjunto de ferramentas para desenvolvimento em Java.

Caso você não tenha acompanhado, publiquei recentemente em um tutorial sobre como instalar o Ubuntu numa máquina virtual. Esta é uma espécie de continuação da série de artigos que visa a configuração de um ambiente de desenvolvimento no Linux. Com esse ambiente, poderemos brincar livremente bom Big Data. Não perca este e os próximos capítulos! 😉

Instalando o JDK 7

Antes de mais nada vamos verificar se, por acaso, já não temos o JDK instalado. Abra o terminal e digite:

javac

install-jdk-02

Note a mensagem de que o programa não foi encontrado. O próprio Ubuntu nos dá uma dica do comando de instalação. Vamos aceitar a sugestão e instalar o pacote openjdk_7-jdk. Digite o comando:

sudo apt-get install openjdk-7-jdk

O terminal irá solicitar a senha. Digite-a.

install-jdk-03

Depois ele vai pedir a confirmação da operação.

install-jdk-04

Pressione Y para aceitar e aguarde o download e a instalação do pacote.

install-jdk-05

Vamos digitar novamente o comando javac para verificar a instalação.

install-jdk-06

Pronto!

Onde está o Java?

Vamos agora conferir onde o Java foi realmente instalado. Para isso, no terminal, digite o comando abaixo para navegar até o diretório onde geralmente ficam as instalações do Java:

cd /usr/lib/jvm

Agora vamos listar os arquivos no diretório com o comando ls:

install-jdk-08

Em nosso exemplo temos duas entradas: java-1.7.0-openjdk-amd64 e java-7-openjdk-amd64.

Ué!? Mas não instalamos apenas uma versão do Java? Sim! Note que cada entrada tem uma cor diferente. O azul mais claro do primerio item indica que ele é um link simbólico (symbolic link), isto é, um simples um atalho. Vamos confirmar com o comando abaixo:

ln -li

install-jdk-09

Notou a seta que demonstra o link apontando para a pasta original?

Enfim, o Java foi instalado no diretório /usr/lib/jvm/java-7-openjdk-amd64, como podemos ver na imagem a seguir:

install-jdk-10

Algumas configurações adicionais

Embora o Java já funcione apenas com a instalação realizada, vamos configurar as variáveis de ambiente para o caso de algum programa procurar o JAVA_HOME.

Vamos novamente verificar se, por acaso, esta variável já existe usando o comando abaixo:

echo $JAVA_HOME

install-jdk-07

Nada. Então vamos defini-la em nossa sessão com o seguinte comando:

JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64

Em seguida, vamos exportar a variável para os demais programas com o comando export:

export JAVA_HOME

Somente como observação, poderíamos ter feito os dois passos anteriores em apenas um comando da seguinte forma:

export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64

Agora vamos testar se a variável está correta:

echo $JAVA_HOME

install-jdk-11

Tudo certo até agora!

O problema é que o comando export não salva a variável de forma permanente, ou seja, se reiniciarmos o computador (ou máquina virtual), perderemos seu valor. A fim de persisti-la e disponibilizá-la para todos os usuários, vamos criar um script que faz a exportação da variável durante a inicialização do ambiente e a torna disponível para todos os usuários do sistema.

Isso é feito com um Shell script no diretório /etc/profile.d. Todos os scripts neste diretório são executados na inicialização.

Primeiro, vamos até o diretório mencionado:

cd /etc/profile.d

install-jdk-12

Para não complicar muito, vamos usar o editor nano para criar um arquivo com permissão de administrador, então execute o seguinte comando:

sudo nano export_vars.sh

No editor, digite a linha abaixo:

export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64

install-jdk-13

Em seguida, pressione CTRL+X para sair do programa (Exit). Ele vai perguntar se você quer salvar o conteúdo. Pressione Y e depois Enter.

Pronto de novo!

O arquivo export_vars.sh foi criado com sucesso. Podemos conferir o conteúdo com o comando cat, da seguinte forma:

cat export_vars.sh

install-jdk-14

Para mais detalhes sobre variáveis de ambiente veja a documentação do Ubuntu.

Considerações finais

Este tutorial tem a intenção de prepará-lo para tópicos mais avançados.

Entretanto, procurei ser detalhado nos comandos utilizados também para que o leitor possa se ambientar no ambiente linux, ao invés de apenas digitar comandos “às cegas” sem compreender o que está fazendo.

O que é Big Data – e por que você deveria estar desesperadamente interessado nisso

Word Cloud "Big Data"

Estima-se que 2,5 quintilhões de bytes de dados são criados todos os dias, criados em sua maioria por usuários na web de forma desestruturada. em diferentes meios (texto, áudio, vídeo, etc.) e fontes (Facebook, Twitter, Youtube, etc.).

O potencial desses dados é gigantesco, mas os bancos de dados relacionais não são capazes de lidar com algo dessa natureza. Qual a solução?

A história dos dados

A forma como os dados são gerados, processados e armazenados mudou drasticamente ao longo das últimas décadas e mesmo em relação aos últimos anos.

Os primeiros cientistas da computação preocupavam-se em criar estruturas de dados otimizadas para armazenar a maior quantidade de informação, da forma mais eficiente possível. Os bancos de dados relacionais ajudaram bastante nessa tarefa.

Porém, o volume de dados que existe no mundo hoje simplesmente não cabe mais nesse modelo. Com a ascensão da Internet disponível, virtualmente, a todo mundo e com a popularização dos sistemas web, incluindo as Redes Sociais, temos hoje um número inimaginável de informação, que aumenta a cada dia.

Agora pare e imagine o potencial desses dados se devidamente analisados. As pesquisas de mercado tradicionais consideram uma população de algumas centenas ou, no máximo, milhares de pessoas para testar alguma tendência.

E se conseguíssemos analisar os dados comportamentais de milhões ou até bilhões de pessoas? A chance de identificar tendências em grupos mais específicos aumentaria enormemente.

O grande problema é que esse volume monstruoso de dados está distribuído em inúmeros locais e em geral não é estruturado para transportarmos tudo para tabelas.

Pense no Twitter. Milhares de pessoas ao redor do mundo postando comentários, críticas, reclamações em forma de um texto com no máximo 140 caracteres. Se pudéssemos analisar sobre o que as pessoas estão mais comentando hoje e se estão falando bem ou mal, teríamos uma enorme vantagem competitiva, não é mesmo? Poderíamos responder “em tempo real” aos consumidores de acordo com o que eles estão querendo.

Texto, som, vídeo, imagem. Há um enorme potencial em tudo isso em busca de uma informação relevante.

O que é Big Data?

Big Data é um termo cunhado por Budhani em 2008 para descrever qualquer conjunto de dados que seja inviável de manipular por uma ferramenta tradicional em um tempo razoável.

Obviamente isso é um tanto vago, mas já existem algumas definições mais específicas.

Os V’s do Big Data

Ao contrário do que o termo pode levar-nos a pensar, Big Data não é apenas sobre o volume de dados. Em tese, um SGBDR tradicional pode tratar quantidades imensas de dados. Big Data envolve outras características relacionadas com velocidade e variedade.

Procurando definir o que é Big Data, alguns pesquisadores chegaram a um conceito de múltiplos atributos. Isso ficou conhecido como os 3 V’s do Big Data: Volume, Velocidade e Variedade.

Entretanto, o amadurecimento da tecnologia deu origem a uma definição mais completa, com 5 V’s: Volume, Velocidade, Variedade, Valor e Veracidade

Confira na imagem abaixo:

Volume

Big Data envolve uma quantidade de dados que começa na casa dos terabytes e chaga até os petabytes. Note que enquanto escrevo esta informação já pode estar desatualizada!

Velocidade

As informações precisam ser transmitidas, processadas e retornadas em tempo hábil para o negócio. Pense na busca do Google, ela precisa “ler a internet” em alguns milissegundos. Ou você usaria o Google se cada pesquisa levasse horas ou dias?

Variedade

Ao contrário dos sistemas de informação mais tradicionais, Big Data gira em torno de uma grande variedade de dados não estruturados e não normalizados.

Muitas informações podem ser obtidas através de processamento de vídeos do youtube, tweets, arquivos de log e páginas de blog.

Eu sei que você já deve estar cansado de eu usar o Google como exemplo, mas farei novamente. Já viu a legenda automática do YouTube? Houve um bom avanço nos algoritmos de análise de áudio, não é? Já pensou em como o Google ordena os resultados da pesquisa, priorizando o conteúdo mais adequado para os termos inputados?

Trabalhar com SEO nada mais é do que tentar influenciar os algoritmos de Big Data do Google!

Valor

Com o amadurecimento das soluções de Big Data, notou-se que as três características já apresentadas não eram razão suficientes para a utilização desta tecnologia.

É necessário que as informações extraídas da massa de dados proporcionem um benefício tangível.

Por exemplo, se uma empresa consegue identificar através de análises estatísticas certas tendências dos consumidores, ela poderá obter uma grande vantagem em relação aos seus concorrentes.

Veracidade

Desde que alguns institutos passaram a usar dados de redes sociais para verificar tendências, surgiu uma nova versão de ataque digital que procura manipular os resultados.

Vou dar um exemplo. Ouvi recentemente que foram criados bots (robôs) para gerar milhares de tweets e postagens na Internet sobre certo candidato a cargo político. O objetivo é fazer parecer que o candidato tem popularidade, sendo “o candidato mais comentado na internet”.

Uma preocupação que tem faltado nas implementações de Big Data é justamente verificar se os dados são oriundos de fontes confiáveis e se os mesmos são autênticos.

Não vou aprofundar-me nesse assunto, mas continuando com o exemplo, uma boa implementação de contagem de tweets poderia eliminar as mensagens repetidas ou muito parecidas. Isso porque spammers são capaz de gerar frases aleatórias com variações de palavras. Portanto a mesma técnica que filtros de spam utilizam deveria ser aplicada nesse caso.

Por quê Big Data?

Até aqui você já deve ter compreendido alguns conceitos básicos sobre Big Data. Porém, ao mesmo tempo, pode estar se perguntando essa tecnologia não teria seu uso limitado a empresas altamente técnicas como o Google.

Em teoria, sobre tudo o que se pode fazer estatística pode se beneficiar de Big Data.

Vejamos agora algumas aplicações reais de grandes e pequenas empresas.

Vendas

O Walmart, por exemplo, adotou Big Data desde muito cedo. Os dados de dez diferentes sites foram consolidados num cluster Hadoop, migrados a partir de bases Oracle e outras fontes de dados.

Além de diminuir o tempo das buscas o Walmart é capaz de verificar se um amigo seu lhe enviou informações sobre um determinado produto em uma rede social e então lhe mandar um e-mail com uma promoção daquele produto.

Ele também pode lhe indicar produtos de acordo com o perfil dos seus amigos no Facebook, de modo que você possa dar um presente adequado.

Telecom

Algumas empresas de Telecom usam Big Data para traçar perfis dos consumidores, unindo padrões de ligação, envio de mensagem e participação em redes sociais. Eles conseguem então descobrir quais consumidores estão mais propensos a trocar de operadora.

Esportes

No campo dos esportes, há várias iniciativas de monitoramento dos atletas. Isso permitirá a análise do desempenho de um time e de cada indivíduo, possibilitará encontrar tendências, prever resultados e até criar jogos mais reais com as características dos times verdadeiros.

Saúde

Na saúde, os pesquisadores poderão prever com mais precisão problemas de saúde baseando-se em históricos hospitalares.

Hoje já existem estudos sobre a coleta de informações de respiração e batimentos cardíacos de bebês nascidos prematuros e o uso de Big Data para identificar padrões de infecção. Isso possibilitará identificar antecipadamente quando um bebê realmente está com infecção antes dos sintomas mais visíveis aparecerem e, consequentemente, aplicar um tratamento mais efetivo.

Trânsito

No Japão, um aplicativo baseado em Big Data está ajudando a melhorar trânsito de uma cidade ao coletar informações de 12 mil táxis e vários sensores.

O serviço consegue analisar 360 milhões de informações sobre o trânsito instantaneamente para retornar a melhor rota naquele horário para o motorista. Com bancos de dados relacionais, o processamento levava vários minutos.

O que realmente é Big Data?

Parafraseando um outro artigo, “no fundo, no fundo, Big Data não se trata de dados nem de tamanho”. Também não se trata de novidades tecnológicas ou descobertas científicas.

Big Data é uma nova forma de ver o mundo, de usar estatísticas e de tomar decisões de negócio.

Uma metáfora seria como a descoberta do microscópio. Uma vez que se consegue enxergar as coisas numa escala completamente diferente de antes, novas descobertas inevitavelmente irão surgir.

A análise criteriosa de um grande volumes de dados é uma tendência que vai continuar e logo se expandirá para mais e mais esferas da vida humana.

Big Data é apenas mais uma modinha?

Algumas pessoas acham que Big Data é apenas mais uma moda tecnológica.

Como toda moda, existe um ciclo onde a princípio todos estão falando sobre aquilo e tem uma expectativa muito alta, depois começam a perceber que o investimento não está trazendo todos os benefícios imaginados, alguns desistem e outros persistem e começam a usar a tecnologia de forma mais adequada e ao final atingem um grau mais real de aproveitamento.

gartner_hype_cycle

Certamente os termos e as tecnologias podem mudar, mas é verdade também que a ideia principal está aí para ficar.

Big Data não é a mesma coisa que Data Warehouse ou Business Intelligence?

Quem conhece um pouco sobre Data Warehouse ou Business Intelligence pode ter encontrado muitos pontos em comum com o que foi apresentado sobre Big Data. Muitos, inclusive argumento que é a mesma coisa. Será?

Por um lado, podemos dizer que a ideia principal é a mesma, a saber, processar um grande volume de dados para ajudar em decisões de negócios e detectar padrões.

Por outro lado, as tecnologias de BI e DW são mais conservadores no que diz respeito às técnicas, tecnologias e estruturas de dados. O foco delas é processar e consolidar os dados estruturados das empresas em uma base de dados somente-leitura separada dos bancos de dados principais dos sistemas a fim de obter estatísticas relevantes.

Já Big Data tem por objetivo unir fontes heterogêneas, privadas e públicas, geralmente em bases NoSQL ou mesmo em arquivos, com dados modificáveis. Big Data também é usado diretamente “em produção”, provendo informações diretamente para os usuários. Os fundamentos também são diferentes, pois Big Data lida especialmente para processamento distribuído, como veremos em breve num artigo sobre MapReduce.

Quais são as tecnologias relacionadas com Big Data?

Não entrarei em detalhes, pois pretendo escrever outros artigos sobre essas tecnologias. Vou simplesmente citar as mais comuns que envolvem o ecossistema do Hadoop, de longe a solução Big Data mais conhecida e também usada como base para outras soluções comerciais.

Hadoop

É uma implementação opensource do framework MapReduce. O Hadoop é um projeto mantido pelo grupo Apache.

Ele é capaz de coordenar tarefas executadas processamento distribuído e paralelo de grandes conjuntos de dados em qualquer quantidade de nós de um cluster.

O Hadoop é implementado em Java, sendo executado numa JVM. Você pode escrever um programa para executar no Hadoop usando a API disponibilizada em jars ou no Maven, inclusive usando sua IDE predileta.

No entanto, Java não é o limite. O Hadoop nada mais é do que uma base de uma pilha de tecnologias, incluindo linguagens de mais alto nível para fins específicos.

HDFS

O Hadoop Distributed File System é um sistema de arquivos distribuído de alta velocidade usado no Hadoop.

YARN

Trata-se de um framework para agendamento de tarefas e gerenciamento do cluster.

HBase

Um banco de dados NoSQL com suporte a dados estruturadas e tabelas grandes.

Hive

O Hive é um tipo de linguagem SQL (HiveQL, para ser mais exato) próprio para realizar consultas em grandes quantidades de dados distribuídos.

Mahout

Este é um projeto que procura unir Inteligência Artificial com Big Data, falando especificamente de Aprendizado de Máquina.

Pig

Pig é uma linguagem de programação de alto nível que facilita na criação de tarefas e análise de dados distribuídos, com execução em paralelo.

Bem, eu sei que você pode ser especialista em “programação porca”, mas não é disso que se trata o Pig. Tá… todo mundo aqui no Brasil já fez essa piada, mas não pude perder a oportunidade! 😉

ZooKeeper

Para manter todos os bichos do Hadoop em suas jaulas é necessário alguém para administrar tudo.

O ZooKeeper é um serviço centralizado para manter a configuração, dar nomes, prover sincronização e agrupamento de serviços.

Mas quem vai operar tudo isso?

Big Data trouxe também um novo tipo de profissional ao palco: o Cientista de Dados.

Uma breve pesquisa sobre esse termo vai trazer muitos resultados, demonstrando que é uma carreira em alta no momento.

Os cientistas de dados devem unir diversas habilidades:

  1. Ser bom em estatística e matemática para analisar corretamente os dados;
  2. Dominar as bases da Ciência da Computação para implementar devidamente as soluções, que geralmente incluem algum tipo de programação e entendimento da arquitetura distribuída; e
  3. Conhecer o negócio da empresa para gerar benefícios tangíveis com os resultados do seu trabalho.

Considerações finais

Sendo uma moda ou não, as empresas estão investindo pesadamente e obtendo resultados concretos com Big Data. Quanto antes uma empresa obtiver os benefícios em potencial de uma solução Big Data, mais ela terá chances de aumentar sua participação no mercado.

Os profissionais que mais cedo dominarem essa tecnologia também terão mais chances de destacar-se nesse nicho de mercado.

Como em toda mudança, os que chegarem por último terão que se contentar com as sobras.

A maior consideração quanto a isso é não entrar no frenesi de implementar Big Data por um fim em si mesmo, nem criar expectativas irreais sobre as tecnologias.

Torna-se necessário então compreender o cenário atual para avaliar cautelosamente o caminho adequado para um investimento em Big Data.

Além disso, voltando ao exemplo do microscópio, a nova visão de mundo proporcionada pela análise de enormes massas de dados deve ser suplementar e não substituta da visão da realidade dos negócios e da realidade humana.

Instalando e configurando o Ubuntu linux numa máquina virtual

virtualbox-ubuntu

Neste tutorial, vamos instalar o Ubuntu, uma das distribuições linux mais populares da atualidade, numa máquina virtual.

Você poderá usar isso para várias finalidades. Aqui no blog, em breve, usaremos em artigos sobre Hadoop e Big Data.

Virtualização

Para quem não está acostumado com virtualização, uma máquina virtual (Virtual Machine, em Inglês) é um ambiente que simula um computador, com sistema operacional próprio, mas que você pode executar dentro do seu sistema atual.

Isso significa que você pode executar um sistema operacional linux dentro do seu Windows e vice-versa. Eu mesmo uso primariamente o Windows 7, mas tenho imagens com XP e diversas distribuições linux.

O sistema operacional principal da máquina é chamado de hospedeiro (host). Os sistemas operacionais usados dentro de máquinas virtuais no sistema hospedeiro são chamadas de sistemas convidados (guests).

Essa técnica tornou-se viável num passado não tão distante quando o hardware atingiu um bom nível de eficiência, inclusive hoje com tecnologia que torna a virtualização quase tão eficiente como um sistema tradicional.

A virtualização traz vários benefícios. O principal é possibilitar a criação da tão famigerada computação em nuvem (cloud computing). Além disso, as empresas que dependem de infraestrutura de TI tanto para desenvolvimento quanto para produção podem usufruir de maior facilidade para a criação de novos ambientes e servidores virtuais, além de flexibilidade para o gerenciamento. Desenvolvedores ou mesmo usuários domésticos como eu podem ter vários servidores com diferentes tecnologias em seu notebook pessoal, inicializados apenas de acordo com a demanda.

Existem ainda sites que disponibilizam ambientes com diversas tecnologias prontos para os administradores usarem em servidores. Um deles é o TurnKey Linux. Baixando imagens de discos virtuais relativamente pequenas, você tem um sistema pronto para uso e somente com o que é necessário para executar a tecnologia escolhida. Enfim, você pode ter um servidor pronto em uma máquina virtual em apenas alguns minutos.

VirtualBox ou VMWare Player?

Os programas gratuitos de virtualização para usuários domésticos mais conhecidos são o VirtualBox da Oracle e o VMWare Player. Ambos são bons produtos, maduros e em constante evolução. Mas com funcionalidades específicas um pouco diferentes, além de vantagens e desvantagens.

Como sou usuário de ambos posso dizer que na prática não há um ganhador absoluto. Depende do uso que fizermos deles. O VMWare, por exemplo, permite copiar e copiar um arquivo do sistema hospedeiro para o convidado e vice-versa. O VirtualBox, por sua vez, traz várias funcionalidades que o VMWare só disponibiliza na versão paga.

Para quem faz questão de uma solução mais completa e possui condições de arcar com as despesas, o melhor seria adquirir uma versão paga do VMWare. Já o usuário doméstico que está começando se dará muito bem com qualquer versão gratuita.

Aqui usaremos o VirtualBox. Mas se alguém optar pelo concorrente não encontrará tanta dificuldade em atingir o mesmo objetivo.

Funcionalidades interessantes do VirtualBox

Existem algumas funcionalidades bem legais quando usamos uma máquina virtual. Irei descrever algumas nas próximas linhas que estão disponíveis no VirtualBox.

Por exemplo, você pode pausar uma máquina virtual a qualquer momento através do menu Máquina > Pausar.

Também é possível salvar snapshots da máquina através do menu Máquina > Criar Snapshot. Sabe o que significa isso? Ao criar um snapshot, você tira uma “fotografia” ou “instantâneo” do sistema naquele momento. Então pode “pintar e bordar”, realizar testes, instalação de programas ou até vírus. Quando cansar da brincadeira, basta restaurar o snapshot e o sistema (disco e memória) voltarão ao estado salvo como se nada tivesse acontecido.

Caso em algum momento você deixe a máquina virtual em tela cheia ou o cursor do mouse seja capturado por ela de forma que você não consiga sair, não se desespere. A tela usada para liberar o mouse e também para algumas teclas de atalho é o CTRL da direita do seu teclado. Este é o padrão e você pode mudá-lo. Essa tecla especial é chamada tecla do hospedeiro, isto é, que permite acessar comandos no sistema hospedeiro. Por exemplo, CTRL+F alterna a máquina virtual entre modo de tela cheia e janela.

Outra funcionalidade interessante, embora deva ser usada com cuidado, é o modo Seamless. Com ele, os programas abertos no sistema dentro da máquina virtual “misturam-se” com a área de trabalho do sistema hospedeiro, dando a impressão de haver apenas um sistema operacional. Veja o seguinte exemplo de um terminal aberto no Ubuntu e exibido em seamless mode:

seamless-mode

Configuração de Hardware

Máquinas mais novas, como o Intel i7, possuem suporte em nível de hardware para virtualização. Entretanto, até algum tempo atrás essas capacidades eram desativadas por padrão. Isso chegava a impedir a virtualização de sistemas operacionais convidados de 64 bits.

Leia o manual da sua placa mãe e do seu processador e verifique se eles possuem suporte nativo para virtualização. Procure por algo como VT-x (Intel) ou AMD-V. Veja um exemplo da BIOS para um processador AMD:

5x3YW

E aqui outro exemplo para um processador Intel:

bios_enablt_vtx1

Lembre-se, sem o suporte nativo, você não será capaz de instalar um sistema operacional de 64 bits como convidado no VirtualBox. Entretanto, se não estou enganado, o VMWare consegue emular via software a virtualização de sistemas 64 bits, mas de qualquer forma o desempenho será sofrível.

Instalando o VirtualBox e as extensões

Acesse a página de downloads e baixe a versão correspondente ao seu sistema operacional.

dowload-virtualbox

Baixe também as extensões para o sistema convidado.

dowload-virtualbox-extension

As extensões trarão várias facilidades, tais como: redimensionamento automático da tela, melhor integração do mouse, compartilhamento de pastas automático entre o sistema hospedeiro e o convidado, uso da USB dentro da máquina virtual e muito mais.

Execute o primeiro arquivo baixado para instalar o VirtualBox. Em geral você não precisa alterar nenhuma configuração, então simplesmente avance até o final da instalação

virtualbox-install

Confirme ainda a instalação de todos os drivers, que serão usados para integrar seus dispositivos como mouse, teclado e rede com a máquina virtual.

Após concluir, execute também o outro arquivo para instalar as extensões do convidado (Guest Additions). O nome deve ser algo como Oracle_VM_VirtualBox_Extension_Pack-4.3.12-93733.vbox-extpack. O programa VirtualBox será aberto. Aceite o contrato para concluir a instalação.

Dando tudo certo, não se esqueça que o atalho adicionado no Menu Iniciar é “Oracle Virtual Box”.

virtualbox-open

Criando uma máquina virtual

Na tela principal do VirtualBox, clique no botão Novo.

virtualbox-main

Na tela de criação, digite “Ubuntu 14”. Note que os demais campos serão preenchidos automaticamente.

configure-vm-01

Clique em Próximo e selecione a quantidade de memória para seu novo ambiente. Aqui vou deixar com 2 Gigabytes (2048 Megabytes), mas uma dica é não ultrapassar 50% da memória total do seu computador.

configure-vm-02

Clique em Próximo. Nesta tela, você poderá criar um novo disco rígido virtual. Um HD virtual é simplesmente um arquivo grande que ficará no seu sistema de arquivos, o qual funcionará como se fosse um HD para o sistema da máquina virtual. A não ser que tenha outros planos, deixe marcada a opção para criar um disco novo.

configure-vm-03

Clique em Criar. Na próxima tela, você poderá escolher o formato do arquivo desse novo disco. Vamos deixar o formato nativo do VirtualBox, o VDI.

configure-vm-04

Clique em Próximo. Nesta tela você pode escolher entre duas opções:

  1. Dinamicamente alocado: nesta opção, o arquivo do disco virtual vai aumentando de tamanho somente quando novos arquivos forem gravados. Isso significa que se você criar um disco de 30 Gigabytes, mas a instalação do SO e os demais arquivos ocuparem apenas 2 Gigabytes, então o arquivo terá apenas 2 Gigabytes. O disco vai aumentando de tamanho na medida do uso até alcançar o limite de 30 Gigabytes.
  2. Tamanho fixo: nesta opção, um disco virtual de 30 Gigabytes vai ocupar todo esse tamanho no seu disco verdadeiro.

Já que economizar espaço nunca é demais, vamos deixar a primeira opção selecionada.

configure-vm-05

Clique em Próximo. Agora vamos selecionar o nome do arquivo e o tamanho do disco virtual.

configure-vm-06

Caso tenha mais de uma partição ou HD no seu computador, você pode mudar o local do arquivo do disco virtual. Em algumas situações já criei máquinas virtuais no meu HD externo. Porém, para este tutorial, vamos apenas deixar tudo como está, pois o padrão é suficiente.

Finalmente, clique em Criar.

Agora você tem um computador virtual para brincar!

virtualbox-virtualmachine-created

Instalando o Ubuntu

Antes de mais nada, acesse a página de downloads da versão desktop do Ubuntu e baixe a versão adequada para o seu computador. Neste tutorial, fiz o download da versão 64 bits, cujo nome do arquivo baixado é ubuntu-14.04-desktop-amd64.iso e possui 964 Megabytes.

ubuntu-download

Com a imagem do disco de instalação do nosso novo sistema operacional, podemos então iniciar a máquina virtual e a instalação.

Na tela principal, selecione a VM (máquina virtual) criada e clique em Iniciar.

Antes da inicialização da VM, o VirtualBox vai saudá-lo com uma tela solicitando o disco de boot. Isso ocorre porque ele verificou que o disco virtual está vazio.

Clique no botão à direita do campo e selecione o arquivo do Ubuntu anteriormente baixado.

vm-boot-disk-select

Clique em Iniciar e aguarde a inicialização da instalação do Ubuntu.

ubuntu-install-01

Você pode selecionar sua língua materna ou deixar em Inglês. Eu prefiro o Inglês porque em TI as traduções acabam por confundir mais que ajudar. Clique em Install Ubuntu ou Instalar Ubuntu, dependendo da sua escolha.

A próxima tela irá informar se o Ubuntu vai executar bem na máquina onde está sendo instalada. Além disso, há opções para já instalar as últimas atualizações e alguns softwares de terceiros. Selecione todas as opções e clique em Continue.

ubuntu-install-02

Agora há opções para formatar ou particionar o disco antes da instalação. Como temos um disco virtual dedicado, simplesmente selecione a primeira opção para formatá-lo e executar uma instalação limpa.

ubuntu-install-03

Clique em Install Now.

Na verdade, a instalação não vai começar ainda. Isso deve ter sido uma grande falha de design. A próxima tela contém a seleção da sua localidade. Digite o nome da capital do seu estado. Coloquei “Sao Paulo”.

ubuntu-install-04

Clique em Continue.

Na próxima tela você pode selecionar o tipo do seu teclado. Teste-o para ver se está ok e clique novamente em Continue.

ubuntu-install-05

Finalmente, digite seus dados de usuário, incluindo a senha, e clique em Continue para iniciar a instalação de verdade.

ubuntu-install-06

Aguarde o processo de instalação.

ubuntu-install-07

Ao final, uma caixa de diálogo vai aparecer informando que o sistema deve ser reiniciado. Clique em Restart Now.

Nota 1: enquanto fazia este tutorial, o Ubuntu travou e não reiniciou corretamente. Então, fui até o menu Máquina > Reinicializar para forçar um reset.

Nota 2: a instalação do Ubuntu ejetou automaticamente o disco de instalação virtual do Ubuntu. Se estiver instalando outro sistema operacional que não faça isso, use o menu Dispositivos > Dispositivos de CD/DVD > Remover disco do drive virtual para não iniciar a instalação do sistema novamente por engano.

Pronto, o sistema está instalado e pronto para uso.

ubuntu-installed

Melhorando a integração entre sistema hospedeiro e convidado

Note que a janela do ubuntu ficou bem pequena, quase inutilizável. Vamos resolver isso!

Lembra que instalamos as “extensões do convidado” (Guest Additions) no VirtualBox? Elas facilitarão o uso da máquina virtual de várias formas, mas falta a parte da instalação no sistema convidado. Isso ocorre para que o VirtualBox consiga “conversar” com o SO que está na máquina virtual.

Para fazer isso, devemos seguir as instruções da documentação do VirtualBox que nos dá alguns comandos.

Vamos abrir o terminal de comandos clicando no primeiro botão à esquerda (equivalente ao “Iniciar” do Windows) e pesquisando na caixa de busca por “terminal”.

search-terminal

Se nada mudou no VirtualBox ou no linux desde que escrevi este tutorial, as instruções do Guest Additions para o Ubuntu consistem nos seguintes comandos:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install dkms

Nota: um usuário comentou que não conseguiu executar o último comando com sucesso, tendo substituído por sudo apt-get install virtualbox-guest-dkms. Isso pode ser necessário se estiverem sendo usadas diferentes configurações ou outras versões do Ubuntu ou ainda outras distribuições linux.

O comando sudo que prefixa os demais não está na documentação, mas é necessário se você não está executando o terminal com privilégios de superusuário (administrador).

O primeiro comando é apt-get update. Ele irá atualizar o índice de pacotes do Ubuntu. Dessa forma ele saberá as últimas versões de todos os seus componentes e programas. Após digitar o comando, o sistema irá solicitar a senha do usuário e então executar a ação.

guest-additions-01

O próximo comando é apt-get upgrade. Ele vai efetivamente instalar todas as atualizações do sistema. Após entrar o comando, o Ubuntu vai solicitar algumas confirmações. Pressione Y (yes) para confirmar a atualização e aguarde.

guest-additions-02

Após a atualização do sistema, executaremos o último comando: apt-get install dkms. Este comando vai instalar o pacote dkms, que possibilita a módulos do kernel serem atualizados independentemente. O Guest Additions precisa disso porque ele é um módulo do Kernel e é atualizado com frequência, caso contrário seria necessário recompilar o Kernel do linux a cada atualização.

guest-additions-03

O comando vai pedir a confirmação da instalação. Pressione Y quando necessário.

Neste momento já cumprimos todos os pré-requisitos para a instalação do Guest Additions. Então vamos à instalação em si.

Acesse o menu Dispositivos > Inserir imagem de CD dos Adicionais para Convidado....

guest-additions-04

Ao acionar o menu, uma imagem de CD do VirtualBox será montada no sistema do Ubuntu e a execução automática (auto run) ocorrerá. Uma mensagem de confirmação será exibida.

guest-additions-05

Clique em Run. A senha será novamente solicitada. Digite-a e aguarde o final da instalação.

guest-additions-06

Finalmente, vamos reiniciar o sistema para ativar o módulo que acabamos de instalar. Clique no botão do sistema no canto superior direito do Ubuntu e selecione a opção Shut Down....

guest-additions-07

Na tela que vai abrir, clique no botão da esquerda para reiniciar.

Após a reinicialização, você poderá, entre outras coisas, redimensionar a janela do VirtualBox como quiser e o Ubuntu irá se ajustar a esse tamanho. Legal, né? Esta é a opção Visualizar > Redimensionar Tela Automaticamente que estava desabilitada anteriormente, mas agora veio ativada por padrão.

Palavras finais

Virtualização é um conceito importantíssimo no mundo de hoje. Desenvolvedores de software não precisam ser especialistas em virtualização, mas devem ter bons conceitos sobre como isso funciona e devem saber usar todos os benefícios a seu favor.

Criar máquinas virtuais não é difícil, basta ter uma base sobre o assunto e saber usar as ferramenta já existentes, que estão cada vez mais intuitivas e poderosas.

Os benefícios da criação de máquinas virtuais são inúmeros, a começar por podermos usufruir de uma variedade de ambientes dentro de um único computador.

Em futuros artigos, pretendo trazer tutoriais envolvendo Hadoop, inclusive com a criação de um cluster, cada um em uma máquina virtual, para processamento Big Data.

Dissecando o padrão de projetos Singleton

singleton O padrão de projetos Singleton consiste em uma forma de garantirmos que teremos uma única instância de uma determinada classe no programa atual.

Por exemplo, em um programa Java para desktop podemos criar um Singleton da classe que gerencia a conexão com o banco de dados.

Este é um dos design patterns mais simples que existem, mas ele possui algumas nuances importantes de se entender do ponto de vista de implementação.

Implementação inicial em Java

Para garantirmos uma única instância de uma classe, a abordagem mais comum é criar um método estático que retorne sempre o mesmo objeto. Exemplo:

public class Singlegon {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton() { }
}

O atributo estático instance armazena o objeto criado para retornar a cada chamada de getInstance().

O trecho private Singleton() { }; é um construtor privado, garantindo que nenhuma outra classe poderá criar inadvertidamente uma instância desta.

Postergando a criação do objeto

Tudo ok, mas nem sempre queremos criar o objeto no modo agressivo (eager), isto é, instanciá-lo logo que a classe é carregada. Em muitas situações, é desejável postergar a criação do objeto até a primeira chamada. Exemplo:

public class Singlegon {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

No código acima, o primeiro acesso ao método getInstance() irá disparar a criação do objeto, que será então retornada nas demais chamadas.

Problemas de sincronizção

O problema do código acima é que se houver mais de uma chamada concorrente no primeiro acesso ao método getInstance() ele pode criar duas instâncias de Teste. Duas threads poderiam entrar dentro do if, certo?

A solução mais básica para isso é sincronizar o método:

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

O problema desta abordagem é que todas as chamadas estarão sujeitas a bloqueios, deixando a execução geral do programa mais lenta. Imagine um método assim num servidor de aplicação com vários usuários acessando o sistema! É terrível.

Uma solução melhor seria um bloco synchronized dentro do if, assim:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            instance = new Singleton();
        }
    }
    return instance;
}

Isso resolve o problema da sincronização em todos os acessos, mas é uma solução “ingênua”, pois na verdade voltamos ao problema inicial. Como o if não está sincronizado, duas threads diferentes podem entrar no bloco de criação ao mesmo tempo e, mesmo com a sincronização, elas retornarão instâncias diferentes quando instance == null.

Então, a solução mais “pura” para o singleton pattern seria acrescentar uma verificação dupla, assim:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

Com esta última abordagem garantimos que não haverá perda de desempenho por causa de sincronização desnecessária do método inteiro.

Além disso, garantimos uma única instância de Teste, pois mesmo que duas chamadas concorrentes entrem dentro do primeiro if, temos uma nova verificação sincronizada.

No pior caso, se houver duas ou mais chamadas concorrentes no primeiro acesso a getInstance (quando INSTANCE ainda é null), apenas estas primeiras chamadas serão sincronizadas, sendo que após a primeira atribuição de INSTANCE, nenhuma chamada posterior será sincronizada.

Além do Singleton: padrão Registry

Alguns argumentam que o padrão Singleton está depreciado e deve ser abandonado. De certa forma eu concordo, pois em ambientes onde múltiplas threads e múltiplas aplicações executam concorrentemente, ter apenas um objeto quase nunca é desejável.

O padrão de projeto Registry permite armazenarmos uma coleção de objetos, cada um contendo um identificador ou um escopo específicos. É como se limitássemos o escopo do Singleton de “um objeto por programa” para “um objeto por qualquer escopo que quisermos“.

A implementação varia muito, mas podemos encontrar exemplos claros desse padrão em:

  • Threadlocal, que permite armazenar valores para uma thread, ou seja, um Singleton para cada uma.
  • HttpSession, que retorna sempre o mesmo objeto para cada usuário do sistema web, ou seja, um Singleton por usuário.
  • Frameworks de Injeção de Dependências como Spring ou CDI, os quais gerenciam a criação de objetos em diferentes escopos e permitem inclusive usar o padrão Singleton declarativamente.

Não entrarei em detalhes sobre o Registry neste artigo.

Indicações de Leitura

Uma leitura mais completa sobre o assunto está no Head First Design Patterns (Use a Cabeça! Padrões de Projeto). Embora tendo suas falhas, este é um livro muito bom para quem ainda está começando a entender Padrões de Projetos.

Outros detalhes interessantes sobre Singleton, como variações de implementação, podem ser encontrados na Wikipédia (em Inglês).

Uma breve definição do padrão Registry pode se encontra no catálogo de padrões do Martin Fowler.

Transações distribuídas e processamento paralelo com Atomikos

distributed Atomikos é um software Java que, entre outras coisas, implementa os padrões JTA (Java Transaction API) e XA (eXtended Architecture, que suporta processamento de transações distribuídas).

Em geral, cada transação é associada à thread atual, de modo que os diversos métodos que atendem uma solicitação num servidor JEE podem compartilhá-la.

Entretanto, uma questão interessante do StackOverflow levantou a possibilidade de uma aplicação dividir uma operação atômica em tarefas delegadas a várias threads, porém compartilhando uma única transação global.

Bem, para fazer esse “desvio” da arquitetura original, a solução foi usar diretamente a API XA do Atomikos para incluir os DataSources das diferentes threads na transação principal.

Fiz um exemplo simples que implementa isso. O projeto está disponível no meu GitHub.

Implementação

Antes de mais nada, temos a inicialização do DataSource e do TransactionManager usando a API do Atomikos realizado na class AtomikosDataSource. Eis o trecho relevante:

// Atomikos implementations
private static UserTransactionManager utm;
private static AtomikosDataSourceBean adsb;

// initialize resources
public static void init() {
    utm = new UserTransactionManager();
    try {
        utm.init();
        adsb = new AtomikosDataSourceBean();
        adsb.setMaxPoolSize(20);
        adsb.setUniqueResourceName("postgres");
        adsb.setXaDataSourceClassName("org.postgresql.xa.PGXADataSource");
        Properties p = new Properties();
        p.setProperty("user", "postgres");
        p.setProperty("password", "0");
        p.setProperty("serverName", "localhost");
        p.setProperty("portNumber", "5432");
        p.setProperty("databaseName", "postgres");
        adsb.setXaProperties(p);
    } catch (SystemException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

Depois, implementei uma thread chamada Processamento que recebe a instância da transação (Transaction) principal. A interface Callable define que a thread é um tipo de tarefa que retorna um valor Integer. Eis o código:

private static class Processamento implements Callable<Integer> {

    private int id;
    private boolean falhar;
    private Transaction transaction;

    public Processamento(int id, boolean falhar, Transaction transaction) {
        this.falhar = falhar;
        this.transaction = transaction;
        this.id = id;
    }

    public Integer call() throws Exception {
        if (falhar) {
            throw new RuntimeException("Falhou inesperadamente!");
        }

        //enlist xa connection
        XAConnection xac = AtomikosDataSource.getDS().getXaDataSource().getXAConnection();
        synchronized (transaction) {
            transaction.enlistResource(xac.getXAResource());
        }

        //normal execution, update row with OK
        Connection c = xac.getConnection();
        Statement s = c.createStatement();
        s.executeUpdate("update teste set processado = 'ok' where id = " + id);
        s.close();
        c.close();

        //delist xa connection
        synchronized (transaction) {
            transaction.delistResource(xac.getXAResource(), XAResource.TMSUCCESS);
        }
        return id;
    }

}

Note que, ao invés de usar o JTA, estou usando diretamente a API do XA implementada pelo Atomikos.

A chamada AtomikosDataSource.getDS().getXaDataSource().getXAConnection() recupera uma conexão do XA, a qual é adicionada à transação principal com o comando transaction.enlistResource(xac.getXAResource()). Esta operação é chamada de alistamento (enlistment). Ao final do processamento da thread, o alistamento é desfeito.

Sincronizei alguns trechos pois obtive aleatoriamente alguns NullPointerException nos testes. Não cheguei a averiguar se é um bug do Atomikos ou se é by design, isto é, o objeto Transaction não é thread-safe.

Finalmente, implementei um método que inicia cinco instâncias da thread de processamento listada acima e posteriormente colhe os resultados. Se uma delas falhar, a transação global é desfeita (rollback). Veja o código abaixo:

public static int processar(boolean falhar) {
    int ok = 0;
    Transaction transaction = null;
    try {

        //start transaction
        AtomikosDataSource.getTM().begin();
        transaction = AtomikosDataSource.getTM().getTransaction();

        //create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(5);
        List<Callable<Integer>> processos = new ArrayList<Callable<Integer>>();

        //create 5 threads, passing the main transaction as argument
        for (int i = 0; i < 5; i++) {
            processos.add(new Processamento(i + 1, i == 4 && falhar, transaction));
        }

        //execute threads and wait
        List<Future<Integer>> futures = executor.invokeAll(processos);

        //count the result; get() will fail if thread threw an exception
        Throwable ex = null;
        for (Future<Integer> future : futures) {
            try {
                int threadId = future.get();
                System.out.println("Thread " + threadId + " sucesso!");
                ok++; 
            } catch (Throwable e) {
                ex = e;
            }
        }

        if (ex != null) {
            throw ex;
        }

        //finish transaction normally
        transaction.commit();

    } catch (Throwable e) {

        e.printStackTrace();
        try {
            //try to rollback
            if (transaction != null) {
                AtomikosDataSource.getTM().rollback();
            }
        } catch (IllegalStateException e1) {
            e1.printStackTrace();
        } catch (SecurityException e1) {
            e1.printStackTrace();
        } catch (SystemException e1) {
            e1.printStackTrace();
        }

    }
    return ok;
}

Note que vários métodos possuem um parâmetro chamado falha. Ele será usado para gerar um cenário onde uma das threads irá gerar um erro e forçar o rollback das alterações das demais threads.

O método processar() retorna a quantidade de “sucessos”, isto é, threads que executaram sem erro, independentemente se a transação foi efetivada ou desfeita. Isso também será usado nos testes.

Testes

Fiz testes tanto de um cenário de sucesso quanto de falha para validar a solução.

No cenário de sucesso, cada uma das cinco threads atualiza uma linha da tabela TESTE com o valor ok e no final o método principal faz o commit da transação.

No cenário de falha, a última thread sempre lança uma exceção, forçando o rollback das demais. Note que a última thread criada não é necessariamente a última a ser executada.

O código de teste ficou muito simples. Veja:

public class AtomikosTest {

    @BeforeClass
    public static void init() {
        //create atomikos transaction manager and data source
        AtomikosDataSource.init();

    }
    @Before
    public void reset() {
        //recreate data of TEST table
        AtomikosDAO.resetTable();
    }

    @AfterClass
    public static void shutdown() {
        //close atomikos resources
        AtomikosDataSource.shutdown();
    }

    @Test
    public void sucesso() {
        //process 5 rows in 5 threads
        int okParcial = AtomikosDAO.processar(false);
        //should return 5 successes
        Assert.assertEquals(5, okParcial);
        //confirms in table, count 5 ok's
        Assert.assertEquals(5, AtomikosDAO.countOk());
    }

    @Test
    public void fail() {
        //process 5 rows in 5 threads, one should fail
        int okParcial = AtomikosDAO.processar(true);
        //should return 4 successes
        Assert.assertEquals(4, okParcial);
        //confirms in table, count zero ok's due to rollback
        Assert.assertEquals(0, AtomikosDAO.countOk());
    }

}

Notas sobre a configuração

Neste projeto, usei o servidor de banco de dados PostgreSQL como o recurso a participar da transação distribuída.

Foi necessário habilitar a configuração max_prepared_transactions no arquivo de configuração postgresql.conf com um valor maior que o número de participantes na transação distribuída. Sem isso, o PostgreSQL não será capaz de participar de transações desta natureza.

Considerações finais

Embora haja um crescente interesse sobre NoSQL e até NewSQL, transações ACID, como disponíveis nos SGBDRs tradicionais, são importantes em muitos cenários. Até por isso existem tutoriais sobre como simular uma transação com o conceito de two-phase commit em bancos de dados não transacionais como MongoDB.

Além disso, é importante ressaltar que cada participante de uma transação distribuída deve ser compatível com o protocolo XA. Infelizmente, alguns drivers de bancos de dados ou outras fontes de dados podem não ser compatíveis. Então, faça sua lição de casa e pesquise antes de sair implementando.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Concorrência e objetos thread-safe

1a

Certamente você já concorreu com outras pessoas em sua vida. Uma vaga na faculdade, um emprego ou mesmo a fila do banco.

Sistemas de software também enfrentam certas restrições e competem por recursos computacionais. Na verdade, cada vez que você acessa um site está concorrendo, de alguma forma, com centenas ou milhares de pessoas. Um dos maiores desafios das empresas que disponibilizam serviços na Internet é atender a todos os acessos simultâneos.

Como um engenheiro de software, pretendo agora fazer uma breve análise microscópica sobre esse problema.

O que pode dar errado?

Em geral, um sistema que visa atender vários usuários ao mesmo tempo irá processar as solicitações paralelamente em vários processos ou threads. O problema começa quando as ações de dois ou mais usuários afetam o mesmo conjunto de dados.

Vamos supor que temos um objeto com um contador compartilhado entre diversas threads:

int contador = 0;
public int incrementa() {
    contador++;
    return contador;
}

Suponha ainda que duas threads T1 e T2 estão rodando em dois processadores de uma mesma CPU. Em um dado momento elas chamam, ao mesmo tempo, o método incrementa() e ocorre a seguinte sequência de execução:

  1. Processo T1 executa a linha contador++ (total = 1)
  2. Processo T2 executa a linha contador++ (total = 2)
  3. Processo T2 retorna o valor de total, ou seja, 2
  4. Processo T1 retorna o valor de total, ou seja, 2

Obviamente isso irá causar um efeito colateral indesejado! 🙁

Note ainda que problemas podem ocorrer mesmo que apenas uma das threads faça modificações e as demais estejam lendo valores, já que elas podem ler dados desatualizados e incorrer em exceções.

Um exemplo básico seria a iteração sobre uma lista. Se uma thread remove um item da lista, outras threads que estão no meio de uma iteração podem acabar em uma exceção ArrayIndexOutOfBoundsException.

Os exemplos são inesgotáveis e os efeitos colaterais os mais bizarros!

Objetos thread-safe

Para resolvermos a situação acima, onde um objeto é acessado por múltiplas threads, precisamos construir um objeto que seja à prova de acesso simultâneo, ou seja, um objeto thread-safe. O que diacho é isso? É um objeto que, em dado contexto de uso, garante o acesso seguro a dados compartilhados por várias threads de forma concorrente sem efeitos colaterais indesejados.

Bem, na verdade, essa é metade da história. O ideal do ponto de vista de desempenho seria que não houvessem acessos concorrentes. Isso significa que, num mundo perfeito, nós conseguiríamos distribuir as tarefas igualmente entre as várias threads e cada uma poderia concluir seu trabalho independentemente, sem uso de recursos compartilhados.

Na prática, é comum precisarmos compartilhar dados, então temos que aplicar um conceito muito popular da vida real: o semáforo.

Em nossas ruas, um semáforo é o que garante que somente os veículos de uma via por vez tenham o direito de atravessar um cruzamento. Em computação, um semáforo nada mais é do que uma variável ou objeto que controla o acesso a um determinado recurso. Com isso, estamos fazendo sincronização do acesso de várias threads, analogamente à sincronização dos vários semáforos de um cruzamento.

Em Java, podemos sincronizar o acesso a um método ou bloco de código. Veja o exemplo:

int contador = 0;
public synchronized int incrementa() {
    contador++;
    return contador;
}

Neste novo exemplo, as duas threads não iriam executar o método incrementa() ao mesmo tempo e no mesmo objeto. Somente uma de cada vez adentraria o bloco com o incremento, evitando o efeito colateral descrito no tópico anterior.

Não se engane! (Ressalvas importantes)

É importante notar que a sincronização no método é equivalente a um bloco synchronized(this), o que significa que o bloqueio é feito na instância do objeto que contém o método. Se houverem duas instâncias, não haverá sincronização.

Um erro muito comum é o desenvolvedor achar que o synchronized irá resolver os problemas de concorrência dos sistemas. Isso não é verdade.

Alguns frameworks web, por exemplo, podem criar várias instâncias das classes para tratar requisições, dependendo de seu escopo, então a sincronização não iria ocorrer como esperado. Além disso, aplicações distribuídas em clusters (dois ou mais servidores) também não seriam sincronizadas mesmo usando singletons.

Mesmo que synchronized fizesse o que alguns esperam, ainda incorreríamos em um grande problema.

Gargalos!

gargalo-e1353923696197

A sincronização resolve alguns problemas, mas pode gerar outros. Se houverem muitos métodos sincronizados, gargalos começarão a surgir no sistema.

Gargalos são pontos do sistema que geram lentidão, pois várias threads precisam ficar esperando sua vez. Quanto mais sincronização fizermos, mais gargalos.

Sincronizando somente o necessário

Para resolver isso, podemos usar uma forma mais adequada de sincronização. No exemplo anterior, usamos um semáforo “global”, isto é, o próprio objeto. Isso faz com que todo acesso ao objeto seja sincronizado por um único semáforo!

A solução para isso é usar semáforos mais específicos de forma que sincronizemos somente o que é necessário. Veja o exemplo:

Integer contador = 0;
Object semaforo = new Object();
public Integer incrementa() {
    synchronized (semaforo) {
        contador++;
        return contador;
    }
}

Este simples exemplo não traz muitas vantagens em si mesmo. Mas considere um cenário onde existam outros métodos nesse objeto que não modificam a mesma variável. Poderíamos criar um semáforo para cada variável, limitando ao máximo o escopo da sincronização.

A boa prática diz que o bloqueio deve ser referente aos dados acessados e não ao objeto que os contém. Isso evita muitos bloqueios desnecessários e pode fazer toda a diferença em cenários um pouco mais complexos.

E se não houver modificações?

Objetos imutáveis são, por natureza, seguros para uso em múltiplas threads, pois não há risco de efeitos colaterais. Eles não precisam ser sincronizados para serem seguramente usados em multithreading e podem ajudar muito na melhoria do desempenho da aplicação.

Este é um dos motivos pelos quais as classes básicas do Java, como String e Integer, são imutáveis. Observe que eu estou falando que não é possível modificar o valor de uma instância dessas classes, não que você não pode atribuir outra instância a uma variável.

Então nunca haverá problemas com objetos imutáveis?

Sim e não! Não é por acaso que a definição de thread-safe fala sobre o contexto de uso.

Apenas para citar um exemplo, suponha que um mapa imutável contenha elementos do tipo ArrayList mutáveis. Se threads diferentes acessarem e modificarem essas listas, estaremos sujeitos novamente a efeitos colaterais indesejados. Então, mesmo uma coleção imutável ou adequadamente sincronizada não cobre todos os casos, pois se ela contém um elemento mutável, ainda que seja um simples POJO, o resultado final não será thread-safe.

É preciso também tomar cuidado com classes do Java que, apesar de aparentarem inocência, na realidade são inerentemente thread-unsafe. Um exemplo é classe SimpleDateFormat, que estende a classe DateFormat, a qual possui um atributo do tipo Calendar, que é mutável. Aliás, ter atributos mutáveis como o Calendar é uma fonte de problemas em potencial.

Em resumo, devemos analisar todos os objetos compartilhados por threads, incluindo seu conteúdo.

Um pouquinho da API do Java

Para tentar ajudar, a API Collections do Java nos mune com várias implementações imutáveis e thread-safe para todos os gostos.

As versões sincronizadas de classes de coleções são encontradas no pacote java.util.concurrent. Eis alguns substitutos para as classes convencionais:

A classe ConcurrentHashMap é uma versão melhorada de mapa sincronizado em relação ao HashTable, suportando manipulação por várias threads, mas sem bloquear as operações de leitura.

O método utilitário Collections.synchronizedMap(map) retorna um wrapper sincronizado de um mapa qualquer. Entretanto, mesmo métodos de leitura são sincronizados, resultando em mais gargalos com relação ao ConcurrentHashMap.

Alguns dos wrappers sincronizados disponíveis são:

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

Além disso, também existe uma lista de wrappers imutáveis, isto é, que retornam uma versão imutável do objeto:

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

A referência oficial dos métodos que retornam wrappers imutáveis e sincronizados está aqui.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Reflexões sobre o uso de frameworks

Construção Errada

Em meu último artigo, Porque eu odeio frameworks, traduzi uma parábola fictícia que critica a crescente complexidade dos frameworks. Se você não tem muita experiência com Design Patterns (Padrões de Projeto) e com diversificados frameworks, provavelmente não tenha entendido ou mesmo achado o texto pouco interessante.

Agora, sendo mais direto, irei analisar nos tópicos a seguir alguns dos problemas e dificuldades que devemos nos precaver quando usamos frameworks e bibliotecas. Escolhas erradas impactam todas as fases do desenvolvimento de um sistema!

Complexidade excessiva

Quando aprendemos sobre Design Patterns e princípios como coesão e desacoplamento, temos a tendência de considerar tudo isso como soluções mágicas para os problemas de complexidade do software. Alguns começam a aplicá-los indiscriminadamente de forma que, mesmo operações que antes eram simples, exigem agora diversas classes para implementação e conhecimentos de vários conceitos para o entendimento.

Quando ainda estamos nessa fase de “descoberta”, onde acreditamos cegamente nas “vantagens”, sem compreender as consequências de nossas decisões ou mesmo a correta aplicação dos padrões, podemos acabar com dezenas de frameworks pendurados em nossos sistemas e inúmeras classes adicionais mesmo para tarefas simples. Já ouviu a expressão “matar uma mosca com um canhão?”

Soluções “genéricas”

Bons frameworks e bibliotecas possuem aplicações bem definidas. Nenhum framework é tão bom e completo a ponto de ser usado em todo e qualquer projeto. Nem mesmo uma determinada arquitetura como o MVC, por exemplo, mas infelizmente não é sempre assim que aprendemos na teoria.

Por outro lado, nós, desenvolvedores, somos culpados muitas vezes de usar um framework ou biblioteca indiscriminadamente, simplesmente porque já temos algum conhecimento.

Comprometimento da Arquitetura

Muitos frameworks são intrusivos no que diz respeito à arquitetura do software. Isso quer dizer que eles nos obrigam a tomar certas decisões arquiteturais simplesmente para que eles funcionem.

Infelizmente, a realidade hoje é que a grande maioria dos sistemas existentes possuem suas regras de negócios codificadas conjuntamente com classes específicas de terceiros. Não sabemos, e talvez nem pensemos sobre isso, o quanto somos orientados a tecnologias. Para falar a verdade, eu reconheço que também sou culpado disso. Quando codificamos, na maior parte das vezes, estamos mais preocupados com questões técnicas do que em realmente atender aos requisitos. Ou pior, quantas vezes não queremos simplesmente concluir o trabalho e não nos preocupamos com a qualidade interna do código? Infelizmente, a “preguiça” do programador em codificar corretamente é um dos grandes fatores que fazem crescer o número de defeitos e o número de horas extra.

Conclusão

Infelizmente, é preciso queimar muitos neurônios para criar frameworks e bibliotecas que, sendo flexíveis, não sejam demasiadamente complexos e intrusivos nos sistemas. E o mesmo se aplica ao uso deles.  Bons programadores devem saber não apenas como configurar, instanciar e executar objetos de frameworks e bibliotecas, mas principalmente sua precisa escolha e aplicação.

Por fim, para divertimento dos nerds de plantão, selecionei um exemplo de um Hello World em Java que abusa um pouco da complexidade. Ele serve para ilustrar como é possível complicar algo tão trivial.

public interface MessageStrategy {
    public void sendMessage();
}

public abstract class AbstractStrategyFactory {
    public abstract MessageStrategy createStrategy(MessageBody mb);
}

public class MessageBody {
    Object payload;
    public Object getPayload() { return payload; }
    public void configure(Object obj) { payload = obj; }
    public void send(MessageStrategy ms) {
        ms.sendMessage();
    }
}

public class DefaultFactory extends AbstractStrategyFactory {
    private DefaultFactory() {}
    static DefaultFactory instance;
    public static AbstractStrategyFactory getInstance() {
        if (null==instance) instance = new DefaultFactory();
        return instance;
    }
    public MessageStrategy createStrategy(final MessageBody mb) {
        return new MessageStrategy() {
            MessageBody body = mb;
            public void sendMessage() {
                Object obj = body.getPayload();
                System.out.println(obj.toString());
            }
        };
    }
}

public class HelloWorld {
      public static void main(String[] args) {
            MessageBody mb = new MessageBody();
            mb.configure("Hello World!");
            AbstractStrategyFactory asf = DefaultFactory.getInstance();
            MessageStrategy strategy = asf.createStrategy(mb);
            mb.send(strategy);
      }
}

* Extraído de http://seenonslash.com/node/465

Porque eu odeio frameworks…

Decidi construir uma prateleira de temperos. Como já fiz pequenos projetos com madeira anteriormente, acredito ter uma boa ideia do que eu vou precisar: um pouco de madeira e algumas ferramentas básicas, como fita métrica, serra, nivelador e um martelo.

Vou até uma loja de ferramentas para comprar o que preciso e pergunto ao atendente onde encontro um martelo.

– Um martelo? – ele pergunta. Na verdade ninguém mais compra martelos. Eles estão meio fora de mora.

Surpreso com essa mudança, pergunto a ele o por quê.

– Bem, o problema com martelos é que existem muitos tipos diferentes: do tipo marreta, o tradicional (com as duas pontas para tirar pregos), o de cabeça arredondada, o de borracha e assim por diante. E se você comprar um tipo de martelo e depois perceber que precisava de outro tipo? Você teria de comprar outro martelo. Sendo assim, a maioria das pessoas queria apenas um único martelo que pudesse executar todos os tipos de marteladas que você possa conhecer em sua vida.

– Suponho que isso seja bom. Pode me mostrar onde encontro o “martelo universal”?

– Não, nós não vendemos ele mais. Estão obsoletos.

– Sério? Mas você acabou de me dizer que o “martelo universal” é a onda do futuro.

– Descobrimos que, se você faz um tipo de martelo capaz de fazer todos os tipos de tarefas de todos os tipos de martelos, ele na verdade não é tão bom em nenhuma tarefa. Pregar um prego com uma marreta não é muito eficiente. E, se você quer matar sua ex-namorada, não há melhor substituto do que o martelo com cabeça arredondada.

– Isso é verdade. Mas então, se ninguém compra mais “martelos universais” e você não vende mais os martelos tradicionais, que tipo de martelos você vende?

– Na verdade, nós não vendemos nenhum martelo.

– Então…

– Conforme nossas pesquisas, o que as pessoas realmente precisavam não era um “martelo universal”, no fim das contas. Sempre foi melhor ter o tipo de martelo adequado para o trabalho. Então nós começamos a vender fábricas de martelos capazes de produzir qualquer tipo de martelo que você possa se interessar em usar. Tudo o que você precisa fazer é preencher a fábrica de martelos com trabalhadores, ativar a maquinaria, comprar a matéria prima dos martelos, pagar as contas e, voilà, você terá exatamente o tipo de martelo que precisa na hora!

– Mas eu não queria comprar uma fábrica inteira de martelos.

– Isso é bom, porque nós também não as vendemos mais.

– Mas você acabou de dizer que…

– Nós descobrimos que a maioria das pessoas realmente não precisa de uma fábrica inteira de martelos. algumas pessoas, por exemplo, nunca precisarão de um martelo com cabeça arredondada (talvez porque não tenham uma ex-namorada ou já usaram um furador de gelo para matá-las). Então, não há razão para alguém comprar uma fábrica de martelos que pode produzir todo tipo de martelo existente debaixo do sol.

– Sim, isso faz muito sentido.

– Então, ao invés disso, nós começamos a vender esquemas para fábricas de martelos, permitindo a nossos clientes construir suas próprias fábricas de martelos, personalizadas para fabricar apenas os tipos de martelos que eles iriam precisar.

– Deixe-me adivinhar: vocês não vendem isso mais…

– Não! Certamente não! Descobrimos que as pessoas não queriam construir uma fábrica inteira apenas para fabricar um ou dois martelos. Deixe a construção da fábrica para os especialistas em construção de fábricas, foi o que eu sempre disse!

– E eu tenho que concordar com você.

– Isso. Então nós paramos de vender os esquemas e começamos a vender fábricas de construção de fábricas de martelos. Cada fábrica de fábricas de martelos é construída para você pelos principais especialistas no ramo de fábrica de fábricas de martelos, então você não precisa se preocupar com nenhum detalhe em construir uma fábrica. E ainda assim você tem todos os benefícios de ter sua própria fábrica personalizada, produzindo seus martelos personalizadas rapidamente, de acordo suas próprias especificações de martelo.

– Bem, na verdade, isso não parece…

– Eu sei o que você vai dizer! Nós não vendemos mais isso também. Por alguma razão, poucas pessoas estavam comprando as fábricas de fábricas de martelos, então nós criamos outra solução para resolver o problema.

– Hu-ho!

– Nós analisamos a situação com calma e olhamos para toda a infraestrutura de ferramentas. Então entendemos que as pessoas estavam frustradas em ter que gerenciar e operar uma fábrica de fábricas de martelos, tanto quanto a fábrica de martelos que era produzida. Esse tipo de trabalho adicional pode ficar muito confuso quando você lida com um cenário provável de também ter que operar uma fábrica de fábricas de fitas métricas, uma fábrica de fábricas de serras e uma fábrica de fábrica de niveladores, sem mencionar o conglomerado de fabricação de chapas de madeira. Quando nós olhamos a situação com a devida atenção, vimos que era tudo muito complexo para alguém que quer apenas construir uma prateleira de temperos.

– Nem me diga!

– Então, nesta semana, nós estamos introduzindo no mercado a fábrica de fábricas de fábricas de ferramentas de propósito geral, de forma que todas suas diferentes fábricas de fábricas de ferramentas podem ser produzidas por uma única fábrica unificada. A fábrica de fábricas de fábricas não irá apenas produzir as fábricas de fábricas de ferramentas que você precisa, mas cada uma das fábricas de fábricas irão produzir fábricas únicas baseadas nas suas especificações personalizadas de ferramentas. O conjunto final de ferramentas que irão emergir desse processo serão as ferramentas ideias para o seu projeto em particular. Você terá exatamente o martelo e a fita métrica que precisa para sua tarefa, tudo com o simples pressionar de um botão (embora você tenha ainda que implantar alguns “arquivos de configuração” para fazer tudo isso funcionar de acordo com suas expectativas).

– Então, você não tem nenhum martelo? Nenhum mesmo?

– Não. Se você quer mesmo uma prateleira de temperos de alto padrão e de acordo com as especificações da indústria, você precisa urgentemente de algo mais avançado do que um simples martelo de uma lojinha de ferramentas.

– Isso é o que todos estão fazendo agora? Todo mundo está usando uma fábrica de fábricas de fábricas de ferramentas de propósito geral, sempre que precisam de um martelo?

– Exato!

– Tudo bem, Acho que vou querer uma dessas também. Se é assim que todos estão fazendo, acho melhor aprender dessa forma.

– Vai ser bom pra você.

– Vem com manual, né?


Trecho extraído de http://discuss.joelonsoftware.com/?joel.3.219431.12

Uma avaliação do play! framework

Há alguns meses, alguém levantou a possibilidade de melhorar o gerenciamento das visitas a clientes na empresa em que trabalho, utilizando um sistema específico.

Consultaram-me sobre a possibilidade de utilizar um sistema opensource, entretanto não encontrei nada nessa linha. Como estava estudando algumas novidades e queria desenvolver algum sistema com elas, no caso o play! framework, vi que a oportunidade era perfeita, pois tratava-se de um sistema de uso interno e de baixíssimo risco, então respondi que em pouco tempo poderia criar um protótipo.

Utilizei o Twitter Bootstrap (que também estava aprendendo) na camada de apresentação e o Hibernate para persistência (depois de desistir do Ebeans, padrão do framework).

O desenvolvimento do protótipo totalmente funcional ocorreu em uma semana. Trata-se de um sistema pequeno, porém como estava usando o play! e o bootstrap pela primeira vez, acredito que foi bem rápido.

Depois das rotineiras mudanças de requisitos e solicitações de melhorias após a apresentação para os usuários, mais dois ou três dias e estava tudo pronto.

O que tem de mais esse tal de play!?

  • Instantâneo: Todas as alterações em telas ou no código java são recompiladas e executadas sem reiniciar o servidor, só é necessário um “refresh”;
  • Depuração: é possível conectar o debugger do Eclipse com a aplicação executando em modo de desenvolvimento, mas nem é tão necessário, pois quando há algum erro no Java ou na tela, a mensagem do framework é tão amigável que mostra, além da pilha, o próprio código fonte e qual o comando ou erro de sintaxe que causou a falha;
  • Sessão: não existe uma session, os únicos valores que são armazenados entre as requisições ficam em cookies criptografados. Isso faz com que a aplicação seja altamente escalável com múltiplas instâncias, embora seja confuso para quem está acostumado com as tecnologias JEE padrão;
  • Threads: não há criação de threads por requisição como nos Servlets, os métodos dos controladores são static e processamentos “demorados” devem ser assíncronos;
  • Totalmente REST: todas as entradas de url são configuradas no arquivo “routes”, como no rails;
GET     /sobre                      controllers.App.sobre()
  • CoC (Convensão sobre Configuração / Convention over Configuration): a ideia é não reinventar a roda, basta executar um comando “play new [nome sistema]” e sair programando, obviamente se você seguir o padrão adotado pelo framework. Já se quiser fazer diferente aí vai ter tanto ou mais trabalho do que num framework tradicional;
  • Builders para as views: as telas, por exemplo, possuem parâmetros e você “invoca” uma tela a partir do código chamando um método de uma classe gerada dinamicamente que recebe os devidos parâmetros, então se você mudar uma tela para receber algo novo, não tem como esquecer de atualizar o código, já que isso vai gerar erros de compilação;
  • Servidor de aplicação: esqueça Tomcat, Glassfish, Weblogic ou Websphere, ele já vem com um servidor leve embutido, o comando “run” inicia a aplicação em modo desenvolvimento e o comando “start” em modo de produção;
  • Ciclo de vida: o framework gerencia o ciclo de vida através de uma ferramenta de build chamada SBT (uma espécie de ANT), gerencia as dependências através do IVY e também é compatível com Maven;
  • Integração com Eclipse: o comando “eclipsify” gera os arquivos necessários para importar o projeto no Eclipse, quando incluir uma nova dependências, basta executar o comando novamente para atualizar o classpath e demais configurações;
  • Banco de dados: integração nativa com JPA, inclusive gerenciando as evoluções da estrutura do banco e gerando a DDL necessária a partir das classes
  • Telas: templates feitos numa mistura de HTML com a liguagem Scala. É simples e rápido para implementar, porém lento demais para compilar.
<select id="cliente" name="cliente">
  @for(opcao <- options(Cliente.options)) {
    <option value="@opcao._1"
      @if(opcao._1 == filtro.cliente) { selected }>@opcao._2</option>
  }
</select>

Pontos negativos

No Eclipse, várias vezes as classes geradas pelos builders não são reconhecidas mesmo dando refresh no projeto e erros em views, por exemplo, são listados mesmo que já estejam corrigidos. Às vezes, só fechando e reabrindo o projeto (usando “Close Project”). Isso não afeta a execução mas fica “feio” ver o projeto com erros que não existem.

A versão 2.x mudou muito em relação à primeira e a maioria dos tópicos encontrados fala da versão 1. Além diso, ela não é uma simples evolução da primeira, mas uma reformulação total, tornando a migração muito difícil. Por exemplo, alguns recursos, tal como a geração de WAR para deploy num container, foram removidos e atualmente só é possível executar a aplicação no servidor embutido.

Apesar do framework suportar Java ou Scala como linguagem de programação, os templates da versão 2 são feitos em Scala por padrão, portanto é necessário aprender um pouco da sintaxe e a curva de aprendizagem aumenta. A linguagem Scala executa na JVM e é eficiente, mas a compilação pode ser bem mais demorada que uma classe comum.

O gerenciamento automático da evolução da DDL pode ser confuso à primeira vista e causar alguns efeitos colaterais se mal utilizada.

Update: Como comentei num post mais recente, uma biblioteca foi removida do repositório, tornando impossível compilar uma versão do sistema com o play 2.0 e obrigando os desenvolvedores a migrar para a versão 2.1. 

Resumo

No geral as implementações dos diversos aspectos do play! framework usam outras ferramentas já bem conhecidos, tal como o Hibernate, a linguagem Scala, o gerenciador de dependências IVY, etc. O uso dessas e outras tecnologias já consagradas é uma ótima base para se desenvolver, aumentando a confiança nos resultados.

Desde que seguidos os padrões do framework e depois que se pega o jeito dele, fica muito fácil sair implementando novos requisitos. O maior problema no começo é se despender do conceito de JSP/Sevlet que está tão arraigado no mundo Java.

Enfim, o play! framework é muito bom para projetos pequenos e rápidos. Para algo maior, com uma vida útil mais longa, maior risco ou com uma equipe que não conheça a tecnologia, não recomendaria no momento, por dois motivos: a documentação não é muito completa e os desenvolvedores não tem um compromisso em manter compatibilidade, corrigir bugs críticos ou implementar funcionalidades que eles não considerem importantes.

Por outro lado, com o estudo de frameworks como este, é possível extrair algumas boas práticas e conceitos importantes, aplicando de outras maneiras mesmo em tecnologias mais tradicionais.

Estudo de caso: experiência com arquitetura RESTful

Trabalhei recentemente num projeto de desenvolvimento de um sistema interno para a empresa, no qual os analistas de suporte, desenvolvimento ou negócios deverão registrar com antecedência as visitas aos clientes e incluir anexos como documentos e e-mails relacionados às visitas.

Tratando-se de um projeto de baixo risco, encontrei uma ótima oportunidade de realizar experimentos arquiteturais e conferir na prática algumas tecnologias que nunca foram usadas por aqui, principalmente relacionadas à arquitetura REST.

O sistema de Agenda de Visitas

O sistema contém uma tela inicial que lista as entradas da agenda, incluindo filtro e ordenação. Os filtros são persistidos em cookies que expiram após alguns dias, então o usuário vê a tela inicial do sistema da mesma forma que havia deixado na última visita.

Para incluir ou alterar uma entrada o usuário deve autenticar-se. Ele somente poderá editar entradas em seu próprio nome a não ser que seja um líder de equipe. Aqui estão requisitos de autenticação e autorização.

Ao incluir ou alterar uma entrada, é possível incluir ou remover anexos, além de alterar sua descrição a qualquer momento.

Um relatório (PDF ou XLS) com as entradas de um período pode ser emitido por qualquer usuário.

O sistema possui log para todas as ações e é possível emitir um relatório de “quem fez o que e aonde” num período de sistema.

sistema-bootstrap

Iniciando com o play! framework

Na primeira iteração do projeto, após a reunião inicial de levantamento de requisitos, adotei o play! framework para criar o primeiro protótipo, o qual fornece uma pilha de tecnologias pré-definidas e promete um desenvolvimento ágil e agradável.

Em geral, gostei bastante do play!. Embora eu acredite ele carece de amadurecimento em muitos aspectos, a codificação e os resultados obtidos são bastante satisfatórios. Depois que se "pega o jeito", novas funcionalidades são agregadas muito rapidamente ao sistema. O framework segue a linha do Rails e utiliza adota fortemente o conceito de CoC (Convention over Configuration – Convenção antes de Configuração), então as aplicações funcionam sem necessidade de criar XML’s ou qualquer configuração complicada. O ponto fraco desse conceito é que se você quiser “fugir” do padrão poderá ter grandes dores de cabeça. A documentação é simples e bem-feita, mas não completa. Precisei andar bastante nos fóruns.

Para as classes de domínio e a persistência das mesmas adotei o padrão DAO com JPA (Hibernate). No mais, os controladores foram feitos no estilo play! e as views em templates Scala (na verdade, um misto de HTML com expressões em linguagem Scala), que são o padrão da versão 2.0 do framework.

A camada de apresentação, renderizada no navegador do usuário, foi implementada em HTML 5 com toques de jQuery. Usei a “estilosa” biblioteca Bootstrap, que fornece componentes visuais muito bons e altamente portáveis entre os diversos navegadores. Nunca me arrependi de tê-la usado.

O desenvolvimento e depuração com o play! merecem destaque, pois o mesmo exibe mensagens muito amigáveis sempre que há problemas em código Java ou nos templates Scala. O resultado nem sempre é perfeito, houve um momento ou outro em que foi difícil achar a causa, mas em geral foi muito mais fácil depurar do que outros frameworks conhecidos.

Os problemas

Após o primeiro ciclo de implementação dos requisitos iniciais, o sistema precisava ser entregue para homologação. Aqui as restrições do play! começaram a surgir. Como toda empresa que trabalha com JEE, usamos como padrão servidores de aplicação como Tomcat, Weblogic e Websphere. Porém, o play! 2.0 não suportava deploy para o formato WAR, embora fosse possível na versão 1.x. As aplicações do play! são distribuídas em pacotes standalone que rodam uma versão do Jetty e escutam por padrão a porta 9000. Para contornar essa limitação criei um manual de instalação exclusivo para essa tecnologia, mas ainda assim houve dificuldade por parte dos analistas de suporte que estão acostumados com servidores de aplicação tradicionais. Além disso, depois de desfeita a confusão, tive que ajudar a configurar a inicialização automática da aplicação num servidor Windows utilizando parâmetros específicos do play! de acesso a banco de dados, porta, etc. Tudo isso poderia ser evitado com a opção de deploy em formato WAR, mas os desenvolvedores optaram por mudar o framework de tal forma que a versão 2 não é compatível com a anterior de forma alguma, nem há um processo natural de migração (upgrade ou downgrade), então descartei essa possibilidade logo de início.

Outra dificuldade foi a falta de uma configuração do path base da aplicação, ou seja, ela só funciona na “raiz” do servidor. O play! gera sempre URLs absolutas. O problema é que a aplicação seria disponibilizada para os diferentes setores da empresa através de um proxy reverso, logo todos os links da aplicação precisariam ser reescritos como http://servidor/aplicacao/. Resolvi a questão usando uma branch do core do framework que permitia alterar a raiz da aplicação através de uma nova configuração e assim os links seriam gerados corretamente. Feito isso, tudo funcionou perfeitamente… pelo menos por um tempo.

A última gota foi quando recebi o retorno da homologação solicitando alterações no sistema (alguns requisitos novos e outros modificados). Parti para a segunda iteração, fiz algumas modificações, mas o play! simplesmente não conseguia mais compilar as classes e views. Depois de algumas horas tentando entender o motivo, descobri que algumas bibliotecas da versão 2.0 foram removidas do repositório Ivy pela própria equipe responsável pelo framework com a justificativa de estarem com problemas. A solução foi migrar para a versão 2.1, porém os problemas com o path base da URL voltaram, pois esta funcionalidade não havia sido incluída na HEAD e não havia uma branch da versão 2.1 com a nova configuração que eu necessitava. O único caminho seria aplicar manualmente as modificações necessárias na versão 2.1 e recompilar o framework… ou mudar de tecnologia.

Brincando com JAX-RS

Em paralelo a essas experiências, também vinha estudando o JAX-RS (Java API for RESTful Web Services) e realizado alguns testes com o framework Jersey. Além de ser uma API bastante fácil de usar, achei promissora em vários sentidos. De certa forma a codificação de controladores em JAX-RS é semelhante ao play! (que também usa REST), o código fica conciso e a assinatura dos métodos é ainda mais flexível.

Decidi apostar numa mudança e, embora ainda enfrentasse o início da curva de aprendizado da API, a migração para JAX-RS com Jersey foi concluída em menos de uma semana. É claro que nem tudo foi um mar de rosas, houveram várias dificuldades. A documentação oficial do Jersey deixa muitos pontos em aberto e a internet está cheia de exemplos defasados pelos fóruns.

Um ponto complicado foram os uploads, pois como já comentei, havia uma funcionalidade de envio de múltiplos arquivos usando o plugin jquery-file-upload e foi difícil encontrar o parâmetro necessário (que é FormDataMultiPart) que funcionasse na versão mais recente do Jersey.

Além disso, tive que estender a classe de tratamento de cookies, mas o culpado (e quase sempre é ele) é o Internet Explorer por não respeitar o cabeçalho MAX-AGE. Outro problema que envolveu o IE e uploads foi relacionado aos nomes dos arquivos enviados para o servidor. O IE envia o caminho absoluto dos arquivos de upload sem “escapar” as barras de diretórios. O resultado obtido pela API do Jersey é algo como c:\diretorio\arquivo.txt. Tanto no caso do cookie quanto do upload, os desenvolvedores do Jersey se negaram a contornar essas situações (leia-se: fazer gambiarras específicas para o IE), logo cabe ao desenvolvedor da aplicação contornar manualmente esses problemas. Pelo menos existem pontos de extensão suficientes para isso, embora carentes de documentação.

No geral fiquei muito satisfeito com o Jersey, principalmente com a possibilidade de estender as classes que permitem transformar o corpo da requisição em objetos Java e vice-versa (usando as interfaces MessageBodyWriter e MessageBodyReader), assim o mapeamento do request para objeto e de objeto para o response fica completamente transparente.

Abandonando JPSs

Outra escolha marcante da segunda iteração foi o uso da biblioteca FreeMarker para escrever a saída em HTML. Encontrei nela uma linguagem de marcação de templates simples, poderosa e extremamente eficiente.

Havia feito algumas pesquisas sobre benchmarks de template engines porque, após conviver bastante tempo com JSP e JSF eu já estava cansado da limitação, da lentidão e das dificuldades encontradas nessas APIs para criar lógicas simples de apresentação. O FreeMarker se mostrou uma das melhores opções, não possuindo dependências, sendo extensível e compacto.

O fato é que o JSF, principalmente nas versões mais novas, até pode já trazer componentes prontos e fáceis de usar, mas na maioria das vezes isso inclui alto grau de acoplamento com a implementação escolhida e com o visual da aplicação. O desenvolvedor que gosta de liberdade e flexibilidade, sem abrir mão da performance, irá preferir uma biblioteca como o FreeMarker ou Grails, que não agregam uma pilha enorme às tecnologias do projeto.

Substituir as marcações Scala pela sintaxe do FreeMarker não foi muito dispendioso depois de ter feito a configuração básica dos templates. A performance é excelente e a maior parte da sintaxe é simples e agradável.

Conclusões

Primeiramente, achei o play! framework promissor, mas em ambientes já dominados por tecnologias JEE ou com determinadas restrições, dificuldades oriundas da imaturidade do projeto podem surgir e acabar com a “mágica” de soluções inovadoras como esta.

A arquitetura REST é excelente para se trabalhar, pois facilita ao desenvolvedor pensar no sistema em termos de serviços. Este é uma das razões que tornaram a migração do play! para o Jersey fácil, pois não foi preciso alterar nenhum URL.

A API JAX-RS, na minha opinião, é uma escolha certeira para a web. Porém, não estou me referindo apenas a enviar e receber dados em JSON ou XML, mas também HTML sobre HTTP.

Não acoplar as diferentes camadas da arquitetura permitiu a troca de tecnologia de forma razoavelmente rápida e com introdução de poucos bugs. Mais um ponto positivo para bibliotecas como o Bootstrap e mais um motivo pelo qual eu não recomendo tecnologias que “amarrem” diferentes as camadas. O planejamento da arquitetura deve levar em consideração o acoplamento entre os componentes utilizados. Se tivesse adotado inicialmente o JSF ou Vaadin, por exemplo, e tentasse trocar de tecnologia, teria que reescrever praticamente a aplicação inteira.

Há frameworks que prometem uma solução completa, uma pilha tecnológica que vai da persistência à visão. Embora sejam bons para desenvolvedores menos experientes ou que simplesmente não querem lidar com decisões arquiteturais, eles não trarão a longo prazo todos os benefícios de uma arquitetura bem pensada e, pelo contrário, poderão se tornar barreiras à evolução do software. A tendência é que a tecnologia estacione no tempo, como é o caso do framework Demoiselle, iniciativa da SERPRO que pretendia criar um modelo completo para o desenvolvimento de aplicações para o governo.

Por outro lado, usar componentes arquiteturais independentes, conectando-os para formar a arquitetura do sistema, apesar de inicialmente ser mais trabalhoso, compensa ao longo do tempo. Uma vez definida uma boa arquitetura básica (por exemplo: persistência, modelo, controle, visão e apresentação) é possível atingir um alto grau de flexibilidade e baixo acoplamento, além de extrair o melhor de cada camada usando a tecnologia adequada e não aquela que vem de “brinde” com o framework. Aliás, essa é a tendência de muitos frameworks modernos. Os desenvolvedores os dividem em módulos e permitem o uso das componentes independentemente uns dos outros. Outra tendência, como no caso do Jersey, que faz mapeamento nativo de e para JSON, XML e JAXB, é o aumento da interoperabilidade e extensibilidade dos frameworks através de APIs e configurações específicas.

Finalmente já é de senso comum que, por melhor que seja um framework, ele não vai atender todos os diferentes requisitos arquiteturais do mercado.

Página 2 de 2

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.