Aspecto Personalizado para Preenchimento Automático de Campos
Para automatizar o preenchimento de campos comuns, definimos uma anotação customizada e um aspecto AOP.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreenchimentoAuto {
TipoOperacao valor();
}
O aspecto intercepta métodos no pacote de mapeadores anotados com @PreenchimentoAuto:
@Slf4j
@Aspect
@Component
public class AspectoPreenchimentoAuto {
@Pointcut("execution(* com.aplicacao.mapper.*.*(..)) && @annotation(com.aplicacao.anotacao.PreenchimentoAuto)")
public void pontoDeCorte(){}
@Before("pontoDeCorte()")
public void executarPreenchimento(JoinPoint juntaDeMetodos){
log.info("Iniciando preenchimento automático de campos comuns...");
AssinaturaMetodo assinatura = (AssinaturaMetodo) juntaDeMetodos.getAssinatura();
PreenchimentoAuto anotacao = assinatura.getMetodo().getAnotacao(PreenchimentoAuto.class);
TipoOperacao tipoOperacao = anotacao.valor();
Object[] parametros = juntaDeMetodos.getArgumentos();
if (parametros == null || parametros.length == 0) {
return;
}
Object entidade = parametros[0];
LocalDateTime momentoAtual = LocalDateTime.now();
Long usuarioLogado = ContextoSessao.obterIdUsuario();
if (TipoOperacao.INSERCAO == tipoOperacao) {
try {
Metodo metodoSetDataCriacao = entidade.getClass().getMetodo("definirDataCriacao", LocalDateTime.class);
Metodo metodoSetUsuarioCriacao = entidade.getClass().getMetodo("definirUsuarioCriacao", Long.class);
Metodo metodoSetDataAtualizacao = entidade.getClass().getMetodo("definirDataAtualizacao", LocalDateTime.class);
Metodo metodoSetUsuarioAtualizacao = entidade.getClass().getMetodo("definirUsuarioAtualizacao", Long.class);
metodoSetDataCriacao.invoke(entidade, momentoAtual);
metodoSetUsuarioCriacao.invoke(entidade, usuarioLogado);
metodoSetDataAtualizacao.invoke(entidade, momentoAtual);
metodoSetUsuarioAtualizacao.invoke(entidade, usuarioLogado);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else if (TipoOperacao.ATUALIZACAO == tipoOperacao) {
try {
Metodo metodoSetDataAtualizacao = entidade.getClass().getMetodo("definirDataAtualizacao", LocalDateTime.class);
Metodo metodoSetUsuarioAtualizacao = entidade.getClass().getMetodo("definirUsuarioAtualizacao", Long.class);
metodoSetDataAtualizacao.invoke(entidade, momentoAtual);
metodoSetUsuarioAtualizacao.invoke(entidade, usuarioLogado);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
A expressão execution(* com.aplicacao.mapper.*.*(..)) seleciona todos os métodos no pacote mapper, enquanto @annotation(...) filtra por anotações específicas. O ponto de corte deve conter apenas regras de correspondência, sem lógica de negócios. As notifciações como @Before executam a lógica real, e JoinPoint fornece informações do método, requerendo conversão para AssinaturaMetodo para detalhes específicos.
Configuração de OSS para Upload de Arquivos
Para armazenar arquivos de forma isolada, configuramos o Alibaba Cloud OSS. As propriedades essenciais são definidas em um arquivo YAML:
servico:
oss-aliyun:
ponto-final: oss-cn-hangzhou.aliyuncs.com
chave-acesso-id: SEU_ID_AQUI
chave-acesso-secreto: SEU_SEGREDO_AQUI
nome-bucket: meu-bucket-dados
Uma classe de propriedades injeta automaticamente esses valores:
@Component
@ConfigurationProperties(prefix = "servico.oss-aliyun")
@Data
public class PropriedadesOssAliyun {
private String pontoFinal;
private String chaveAcessoId;
private String chaveAcessoSecreto;
private String nomeBucket;
}
Uma classe de configuração cria o utilitário de upload, usando @ConditionalOnMissingBean para evitar duplicação:
@Configuration
@Slf4j
public class ConfiguracaoOss {
@Bean
@ConditionalOnMissingBean
public UtilitarioOssAliyun criarUtilitario(PropriedadesOssAliyun propriedades) {
log.info("Criando instância do utilitário de upload: {}", propriedades);
return new UtilitarioOssAliyun(propriedades.getPontoFinal(),
propriedades.getChaveAcessoId(),
propriedades.getChaveAcessoSecreto(),
propriedades.getNomeBucket());
}
}
Anotações como @Bean registram métodos como beans no contêiner Spring, similar a @Service, mas com foco em configuração. O controlador utiliza o utilitário para upload:
@RestController
@RequestMapping("/api/admin/comum")
public class ControladorComum {
@Autowired
private UtilitarioOssAliyun utilitarioOss;
@PostMapping("/envio-arquivo")
public ResponseEntity<String> enviarArquivo(MultipartFile arquivo) {
try {
String nomeOriginal = arquivo.getOriginalFilename();
String extensao = nomeOriginal.substring(nomeOriginal.lastIndexOf("."));
String nomeUnico = UUID.randomUUID().toString() + extensao;
String urlRetorno = utilitarioOss.upload(arquivo.getBytes(), nomeUnico);
return ResponseEntity.ok(urlRetorno);
} catch (IOException e) {
return ResponseEntity.status(500).body("Falha no envio");
}
}
}
Design de Banco de Dados com Chaves Estrangeiras
Chaves estrangeiras garantem integridade referencial entre tabelas. Em um sistema de delivery, ao salvar um prato com sabores, inserimos primeiro o prato para obter seu ID gerado:
@Transactional
public void salvarPratoComSabores(DtoPrato dtoPrato) {
Prato prato = new Prato();
BeanUtils.copyProperties(dtoPrato, prato);
repositorioPrato.inserir(prato);
Long idPrato = prato.getId();
List<Sabor> sabores = dtoPrato.getSabores();
if (sabores != null && !sabores.isEmpty()) {
sabores.forEach(sabor -> sabor.setIdPrato(idPrato));
repositorioSabor.inserirEmLote(sabores);
}
}
A instrução SQL para inserção do prato usa useGeneratedKeys para retoranr o ID:
<insert id="inserir" useGeneratedKeys="true" keyProperty="id">
INSERT INTO prato (nome, id_categoria, preco, imagem, descricao, data_criacao, data_atualizacao, usuario_criacao, usuario_atualizacao, status)
VALUES (#{nome}, #{idCategoria}, #{preco}, #{imagem}, #{descricao}, #{dataCriacao}, #{dataAtualizacao}, #{usuarioCriacao}, #{usuarioAtualizacao}, #{status})
</insert>
Para sabores, uma inserção em lote:
<insert id="inserirEmLote">
INSERT INTO sabor_prato (id_prato, nome, valor) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.idPrato}, #{item.nome}, #{item.valor})
</foreach>
</insert>
Consultas SQL em XML com MyBatis
Em consultas complexas, resultType define o tipo Java de retorno, enquanto parameterType (opcional) especifica o tipo do parâmetro. Para múltiplos parâmetros, use @Param. Exemplo de consulta paginada com junção:
<select id="consultaPaginada" resultType="com.aplicacao.vo.PratoVisualizacao">
SELECT p.*, c.nome AS nomeCategoria
FROM prato p
LEFT JOIN categoria c ON p.id_categoria = c.id
<where>
<if test="nome != null">
AND p.nome LIKE CONCAT('%', #{nome}, '%')
</if>
<if test="idCategoria != null">
AND p.id_categoria = #{idCategoria}
</if>
<if test="status != null">
AND p.status = #{status}
</if>
</where>
ORDER BY p.data_criacao DESC
</select>
A junção LEFT JOIN retorna todos os registros da tabela esquerda (prato) e combina com a direita (categoria), preenchendo com NULL onde não houver correspondência. A cláusula WHERE filtra condições, enquanto ON define a condição de junção.