Implementação de Aspectos Personalizados e Configuração de OSS em um Sistema de Delivery

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.

Tags: Spring Boot MyBatis Aspect-Oriented Programming java Alibaba Cloud

Publicado em 6-19 19:35