Seu método com @Bean não é tão simples

Quase todo projeto que participamos somos obrigados a disparar emails. Para não ficar mandando emails de verdade sempre configuro dois métodos de fábrica, um associado com o profile de dev e outro com o profile de produção.

	@Bean
	@Profile({"production"})
	public MailSender mailSender(AccessEnvironment accessEnvironment) {
	
		JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl();
		javaMailSenderImpl.setHost("smtp.gmail.com");
		javaMailSenderImpl.setPassword("xxxx");
		javaMailSenderImpl.setPort(587);
		javaMailSenderImpl.setUsername("xxxxx");
		Properties mailProperties = new Properties();
		mailProperties.put("mail.smtp.auth", true);
		mailProperties.put("mail.smtp.starttls.enable", true);
		javaMailSenderImpl.setJavaMailProperties(mailProperties);
		
		return javaMailSenderImpl;
	}
	
	@Bean
	@Profile({"dev","homolog"})
	public MailSender mailSenderDev(AccessEnvironment accessEnvironment) {
	
		return new MailSender() {
			
			@Override
			public void send(SimpleMailMessage[] simpleMessages) throws MailException {
				System.out.println(Arrays.toString(simpleMessages));
			}
			
			@Override
			public void send(SimpleMailMessage simpleMessage) throws MailException {
				System.out.println(simpleMessage);
			}
		};
	}

Só que se você quiser enviar emails em HTML, ao invés de trabalhar com a interface MailSender, vai precisar trabalhar com a interface JavaMailSender. É ela que fornece os métodos para que possamos criar um objeto do MimeMessage, onde podemos definir qual o formato que desejamos enviar o email. Com essa alteração o código fica mais ou menos como segue:

	@Bean
	@Profile({"dev","homolog"})
	public JavaMailSender mailSenderDev() {		
				
		return new JavaMailSender() {
			
			@Override
			public void send(MimeMessagePreparator... mimeMessagePreparators)
					throws MailException {
				throw new UnsupportedOperationException("Nao implementado para dev");
			}
			
			@Override
			public void send(MimeMessagePreparator mimeMessagePreparator)
					throws MailException {
				throw new UnsupportedOperationException("Nao implementado para dev");				
			}
			
			@Override
			public void send(MimeMessage... mimeMessages) throws MailException {
				for (MimeMessage mimeMessage : mimeMessages) {
					send(mimeMessage);
				}
			}
			
			@Override
			public void send(MimeMessage mimeMessage) throws MailException {
				try {
					System.out.println(mimeMessage.getContent());
				} catch (IOException | MessagingException e) {
					log.error("Problema no envio do email para dev, {}",e);	
				}				
			}
			
			@Override
			public MimeMessage createMimeMessage(InputStream contentStream)
					throws MailException {
				//como criar um MimeMessage?
			}
			
			@Override
			public MimeMessage createMimeMessage() {
				//como criar um MimeMessage?
			}
		
			
			@Override
			public void send(SimpleMailMessage... simpleMessages) throws MailException {
				System.out.println(Arrays.toString(simpleMessages));
			}
			
			@Override
			public void send(SimpleMailMessage simpleMessage) throws MailException {
				System.out.println(simpleMessage);
			}
		};
	}	
	
	@Bean
	@Profile({"prod"})
	public JavaMailSender mailSender() {
		JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl();
		javaMailSenderImpl.setHost("smtp.gmail.com");
		javaMailSenderImpl.setPassword("xxxxx");
		javaMailSenderImpl.setPort(587);
		javaMailSenderImpl.setUsername("xxxxx");
		Properties mailProperties = new Properties();
		mailProperties.put("mail.smtp.auth", true);
		mailProperties.put("mail.smtp.starttls.enable", true);
		javaMailSenderImpl.setJavaMailProperties(mailProperties);
		
		return javaMailSenderImpl;
	}

Só que agora temos um problema. O nosso método que funciona para ambiente de desenvolvimento precisa devolver um objeto do tipo JavaMailSender que saiba construir objetos do tipo MimeMessage. Não que seja impossível, mas é um código um tanto quanto chato. Para tentar resolver isso, podemos recorrer ao principio simples da composição. Fazemos com que a implementação de dev utilize o objeto real e, dessa forma, só precisamos substituir os métodos que enviam o email em si.

	@Bean
	@Profile({"dev","homolog"})
	public JavaMailSender mailSenderDev() {				
		
		return new JavaMailSender() {

			JavaMailSender mailSender = mailSender();
			
			@Override
			public void send(MimeMessagePreparator... mimeMessagePreparators)
					throws MailException {
				throw new UnsupportedOperationException("Nao implementado para dev");
			}
			
			@Override
			public void send(MimeMessagePreparator mimeMessagePreparator)
					throws MailException {
				throw new UnsupportedOperationException("Nao implementado para dev");				
			}
			
			@Override
			public void send(MimeMessage... mimeMessages) throws MailException {
				for (MimeMessage mimeMessage : mimeMessages) {
					send(mimeMessage);
				}
			}
			
			@Override
			public void send(MimeMessage mimeMessage) throws MailException {
				try {
					System.out.println(mimeMessage.getContent());
				} catch (IOException | MessagingException e) {
					log.error("Problema no envio do email para dev, {}",e);	
				}				
			}
			
			@Override
			public MimeMessage createMimeMessage(InputStream contentStream)
					throws MailException {
				return mailSender.createMimeMessage(contentStream);
			}
			
			@Override
			public MimeMessage createMimeMessage() {
				return mailSender.createMimeMessage();
			}
		
			
			@Override
			public void send(SimpleMailMessage... simpleMessages) throws MailException {
				System.out.println(Arrays.toString(simpleMessages));
			}
			
			@Override
			public void send(SimpleMailMessage simpleMessage) throws MailException {
				System.out.println(simpleMessage);
			}
		};
	}	
	
	@Bean
	@Profile({"prod"})
	public JavaMailSender mailSender() {
		JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl();
		javaMailSenderImpl.setHost("smtp.gmail.com");
		javaMailSenderImpl.setPassword("xxxx");
		javaMailSenderImpl.setPort(587);
		javaMailSenderImpl.setUsername("xxxxx");
		Properties mailProperties = new Properties();
		mailProperties.put("mail.smtp.auth", true);
		mailProperties.put("mail.smtp.starttls.enable", true);
		javaMailSenderImpl.setJavaMailProperties(mailProperties);
		
		return javaMailSenderImpl;
	}

Olhando o código acima, se a gente pudesse fazer uma aposta, o que você acha que aconteceria? Daria algum problema? Funcionaria tudo tranquilamente? O fato é que quando iniciamos a aplicação tomamos a seguinte exception:

	Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate 
		[org.springframework.mail.javamail.JavaMailSender]: Factory method 
		'mailSenderDev' threw exception; nested exception is 
		org.springframework.beans.factory.NoSuchBeanDefinitionException: 
		No bean named 'mailSender' is defined

		at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:189) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
		at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
		... 75 common frames omitted
		
	Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'mailSender' is defined
		at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:698) 
			~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
		...

Eu não acho que essa seja uma exception simples de ser entendida. O Spring está reclamando que não existe um bean configurado com o nome mailSender. Em primeiro lugar, o Spring assume que o nome do método de fábrica é justamente o nome de registro do objeto gerenciado pelo Spring. A segunda coisa, e mais importante. Onde é que estamos pedindo pela injeção explícita do bean registrado como  “mailSender”? Isso não é feito em nenhum lugar na aplicação! Qualquer pedido de injeção é feito pela própria interface.

	@Service
	public class CentralEmails {

		@Autowired
		private JavaMailSender mailer;
		@Autowired
		private EmailTemplateLoader templateLoader;
		@Autowired
		private HostUrlBuilder urlBuilder;

		...

	}	

Para ficar claro o que acontece, precisamos colocar um simples syso dentro do método anotado com o profile de dev.

	@Bean
	@Profile({"dev","homolog"})
	public JavaMailSender mailSenderDev() {				
		System.out.println(this.getClass());
		return new JavaMailSender() {
			...
		}		
	}	

Quando iniciamos a aplicação é impresso algo assim: class Boot$$EnhancerBySpringCGLIB$$92496cd0. Perceba que a própria classe anotada @SpringBootApplication já é gerenciada pelo Spring. É justamente por isso que você passa ela como argumento para o método run, da classe SpringApplication. O objeto instanciado em função dessa classe na verdade é um proxy. Quando estamos lidando com proxies, você acha que está invocando um método, mas na verdade pode ser que esteja acontecendo algo completamente diferente.

É exatamente isso que acontece quando você invoca o método público, anotado com @Bean, de uma classe gerenciada pelo Spring. A invocação sugere que o Spring deve recuperar o bean registrado com o mesmo nome do método, o que não é verdade, já que nem subimos o projeto em profile de produção. Essa é a causa da exception.

Para contornar podemos isolar a criação do objeto de envio de email real em um método privado e aproveitá-lo tanto para dev(delegate) quanto para produção.

	@Bean
	@Profile({"dev","homolog"})
	public JavaMailSender mailSenderDev() {				
		System.out.println(this.getClass())
		return new JavaMailSender() {

			JavaMailSender mailSender = gmail();
			
			@Override
			public void send(MimeMessagePreparator... mimeMessagePreparators)
					throws MailException {
				throw new UnsupportedOperationException("Nao implementado para dev");
			}
			
			@Override
			public void send(MimeMessagePreparator mimeMessagePreparator)
					throws MailException {
				throw new UnsupportedOperationException("Nao implementado para dev");				
			}
			
			@Override
			public void send(MimeMessage... mimeMessages) throws MailException {
				for (MimeMessage mimeMessage : mimeMessages) {
					send(mimeMessage);
				}
			}
			
			@Override
			public void send(MimeMessage mimeMessage) throws MailException {
				try {
					System.out.println(mimeMessage.getContent());
				} catch (IOException | MessagingException e) {
					log.error("Problema no envio do email para dev, {}",e);	
				}				
			}
			
			@Override
			public MimeMessage createMimeMessage(InputStream contentStream)
					throws MailException {
				return mailSender.createMimeMessage(contentStream);
			}
			
			@Override
			public MimeMessage createMimeMessage() {
				return mailSender.createMimeMessage();
			}
		
			
			@Override
			public void send(SimpleMailMessage... simpleMessages) throws MailException {
				System.out.println(Arrays.toString(simpleMessages));
			}
			
			@Override
			public void send(SimpleMailMessage simpleMessage) throws MailException {
				System.out.println(simpleMessage);
			}
		};
	}	
	
	@Bean
	@Profile({"prod"})
	public JavaMailSender mailSender() {
		return gmail();
	}

	private JavaMailSender gmail() {
		JavaMailSenderImpl javaMailSenderImpl = new JavaMailSenderImpl();
		javaMailSenderImpl.setHost("smtp.gmail.com");
		javaMailSenderImpl.setPassword("xxxx");
		javaMailSenderImpl.setPort(587);
		javaMailSenderImpl.setUsername("xxxx");
		Properties mailProperties = new Properties();
		mailProperties.put("mail.smtp.auth", true);
		mailProperties.put("mail.smtp.starttls.enable", true);
		javaMailSenderImpl.setJavaMailProperties(mailProperties);
		
		return javaMailSenderImpl;
	}	

Os métodos privados não entram no proxy e, por conta disso, não nos causam nenhum problema :).

Sempre tento variar entre posts mais próximos de códigos que vocês efetivamente usem no cotidiano e alguns que sejam mais sobre detalhes internos do framework. Espero que este tenha te ajudado a entender um pouco do que acontece com seus métodos anotados com @Bean.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s