SpringBoot整合JavaMail邮件的两种方式

 更新时间:2024年05月15日 11:13:07   作者:陌殇殇  
这篇文章主要介绍了SpringBoot整合JavaMail邮件的两种方式,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

JavaMail邮件发送

1.方式一:SpringBoot整合JavaMailSender

1.yml配置提取

 application:  
    mail:
        smtp:
            #服务器主机名
            host: smtp.qq.com
            #服务器端口 
            port: 465
            #你的邮箱地址
            username: 5XXXxxxx1@qq.com
            #你的授权码
            password: xxxxx
            #编码格式
            defaultEncoding: utf-8
            #是否进行用户名密码校验
            auth: true
            #设置超时时间
            timeout: 20000
            #是否启用ssl
            ssl-enabled: true
/**
 * 邮件配置
 * @author li
 */
@Data
@ConfigurationProperties(prefix = "application.mail.smtp")
public class MailOptionProperties {
    /**
     * 服务器主机名
     */
    private String host;
    /**
     * 服务器发送端口
     */
    private String port;
    /**
     * 邮箱地址
     */
    private String username;
    /**
     * 授权码
     */
    private String password;
    /**
     * 编码格式
     */
    private String defaultEncoding;
    /**
     * 是否进行用户名密码校验
     */
    private String auth;
    /**
     * 设置超时时间
     */
    private String timeout;
    /**
     * ssl启用
     */
    private boolean sslEnabled;
}

2.config配置

/**
 * 邮件配置
 * @author li
 */
@Configuration
@EnableConfigurationProperties({MailOptionProperties.class})
public class JavaMailConfig {
    @Autowired
    private MailOptionProperties mailOptionProperties;
    @Bean
    public JavaMailSender javaMailSender() throws GeneralSecurityException {
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
        javaMailSender.setHost(mailOptionProperties.getHost());
        javaMailSender.setUsername(mailOptionProperties.getUsername());
        javaMailSender.setDefaultEncoding(mailOptionProperties.getDefaultEncoding());
        javaMailSender.setPassword(mailOptionProperties.getPassword());
        Properties properties = new Properties();
        //开启认证
        properties.setProperty("mail.smtp.auth", mailOptionProperties.getAuth());
        //设置链接超时
        properties.setProperty("mail.smtp.timeout", mailOptionProperties.getTimeout());
        //启用调试
        properties.setProperty("mail.debug", "true");
        //设置端口
        properties.setProperty("mail.smtp.port", mailOptionProperties.getPort());
        //是否启用ssl
        if(mailOptionProperties.getSslEnabled()){
            //设置ssl端口
            properties.setProperty("mail.smtp.socketFactory.port", mailOptionProperties.getPort());
            properties.setProperty("mail.smtp.socketFactory.fallback", "false");
            properties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            //设置ssl加密
            MailSSLSocketFactory sf = new MailSSLSocketFactory();
            sf.setTrustAllHosts(true);
            properties.put("mail.smtp.ssl.enable", mailOptionProperties.getSslEnabled());
            properties.put("mail.smtp.ssl.socketFactory", sf);
        }
        javaMailSender.setJavaMailProperties(properties);
        return javaMailSender;
    }
}

3.工具类

import java.io.File;
import java.util.Date;
import java.util.List;
/**
 * mail邮件消息封装
 * @author li
 */
@Data
public class MailMessage {
    /**
     * 收件人邮箱地址集合
     */
    private List<String> recipient;
    /**
     * 抄送人
     */
    private String [] cc;
    /**
     * 密送人
     */
    private String [] bcc;
    /**
     * 发送时间
     */
    private Date sendDate;
    /**
     * 邮件主题
     */
    private String subject;
    /**
     * 邮件文本内容
     */
    private String text;
    /**
     * 是否html格式打开
     */
    private boolean isHtml = true;
    /**
     * 附件集合
     */
    private List<File> fileList;
}
import java.io.InputStream;
/**
 * 文件数据封装
 */
@Data
public class FileData {
    private InputStream inputStream;
    private String fileName;
    public FileData() {
    }
    public FileData(InputStream inputStream, String fileName) {
        this.inputStream = inputStream;
        this.fileName = fileName;
    }
}
/**
 * 邮件发送工具类
 *
 * @author li
 * @date 2024-05-14
 */
@Component
@EnableConfigurationProperties({MailOptionProperties.class})
public class JavaMailOperation {
    static {
        //防止附件名太长被截断
        System.getProperties().setProperty("mail.mime.splitlongparameters", "false");
    }
    @Autowired
    private JavaMailSender javaMailSender;
    /**
     * 发件人邮箱地址
     */
    @Autowired
    private MailOptionProperties mailOptionProperties;
    /**
     * 邮件发送
     *
     * @param text         内容
     * @param subject      标题
     * @param emailAddress 收件人
     */
    public MimeMessageHelper createMessageHelper(MimeMessage message, String subject, String text, String emailAddress) throws Exception {
        MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");
        // 发件人
        messageHelper.setFrom(new InternetAddress(mailOptionProperties.getUsername(), mailOptionProperties.getPersonal(), "UTF-8"));
        // 收件人邮箱
        messageHelper.setTo(new InternetAddress(emailAddress));
        // 邮件的主题
        messageHelper.setSubject(subject);
        // 邮件的文本内容,true表示文本以html格式打开
        messageHelper.setText(text, true);
        return messageHelper;
    }
    /**
     * 邮件发送
     *
     * @param text         内容
     * @param subject      标题
     * @param emailAddress 收件人
     * @param cc           抄送
     * @param bcc          密送
     */
    public MimeMessageHelper createMessageHelper(MimeMessage message, String subject, String text,
                                                 String emailAddress, String cc, String bcc) throws Exception {
        MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");
        // 发件人
        messageHelper.setFrom(new InternetAddress(mailOptionProperties.getUsername(), mailOptionProperties.getPersonal(), "UTF-8"));
        // 收件人邮箱
        messageHelper.setTo(new InternetAddress(emailAddress));
        //抄送
        messageHelper.setCc(cc);
        //密送(不会在邮件收件人名单中显示出来)
        messageHelper.setBcc(bcc);
        //发送时间
        messageHelper.setSentDate(new Date());
        // 邮件的主题
        messageHelper.setSubject(subject);
        // 邮件的文本内容,true表示文本以html格式打开
        messageHelper.setText(text, true);
        return messageHelper;
    }
    public MimeMessageHelper createMessageHelper(MimeMessage message, String subject, String text, String[] emailAddress) throws Exception {
        MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");
        // 发件人
        messageHelper.setFrom(new InternetAddress(mailOptionProperties.getUsername(), mailOptionProperties.getPersonal(), "UTF-8"));
        // 收件人邮箱
        messageHelper.setTo(emailAddress);
        // 邮件的主题
        messageHelper.setSubject(subject);
        // 邮件的文本内容,true表示文本以html格式打开
        messageHelper.setText(text, true);
        return messageHelper;
    }
    /**
     * 邮件发送-单人
     *
     * @param text         内容
     * @param subject      标题
     * @param emailAddress 收件人
     */
    public void sendMail(String subject, String text, String emailAddress) throws Exception {
        // 创建邮件对象
        MimeMessage message = javaMailSender.createMimeMessage();
        createMessageHelper(message, subject, text, emailAddress);
        //  发送邮件
        javaMailSender.send(message);
    }
    /**
     * 邮件发送-多收件人
     *
     * @param text         内容
     * @param subject      标题
     * @param emailAddress 收件人
     */
    public void sendMail(String subject, String text, String[] emailAddress) throws Exception {
        // 创建邮件对象
        MimeMessage message = javaMailSender.createMimeMessage();
        createMessageHelper(message, subject, text, emailAddress);
        //  发送邮件
        javaMailSender.send(message);
    }
    /**
     * 邮件发送(带附件)
     *
     * @param text         内容
     * @param subject      主题
     * @param fileName     文件名
     * @param inputStream  文件流
     * @param emailAddress 收件人
     */
    public void sendMail(String subject, String text, String fileName,
                         String emailAddress, InputStreamSource inputStream) throws Exception {
        // 创建邮件对象
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper messageHelper = createMessageHelper(message, subject, text, emailAddress);
        // 添加附件
        messageHelper.addAttachment(fileName, inputStream);
        //  发送邮件
        javaMailSender.send(message);
    }
    /**
     * 邮件发送(带附件)
     *
     * @param text         内容
     * @param subject      标题
     * @param file         文件
     * @param emailAddress 收件人
     */
    public void sendMail(String subject, String text, String emailAddress, File file) throws Exception {
        // 创建邮件对象
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper messageHelper = createMessageHelper(message, subject, text, emailAddress);
        // 添加附件
        messageHelper.addAttachment(file.getName(), file);
        //  发送邮件
        javaMailSender.send(message);
    }
    public void sendMail(String subject, String text, String emailAddress, List<File> files) throws Exception {
        // 创建邮件对象
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper messageHelper = createMessageHelper(message, subject, text, emailAddress);
        // 添加附件
        for (File file : files) {
            messageHelper.addAttachment(file.getName(), file);
        }
        //  发送邮件
        javaMailSender.send(message);
    }
    /**
     * 发送freemarker模板格式邮件
     *
     * @param text         内容
     * @param subject      标题
     * @param emailAddress 收件人
     * @param files        附件
     */
    public void sendFreemarkerMail(String subject, String text, String emailAddress, List<File> files) throws Exception {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper messageHelper = createMessageHelper(message, subject, text, emailAddress);
        // 构建 Freemarker 的基本配置
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_0);
        configuration.setClassLoaderForTemplateLoading(this.getClass().getClassLoader(), "templates");
        Template template = configuration.getTemplate("mail.ftl");
        // 模板渲染
        StringWriter out = new StringWriter();
        Map<String, Object> param = new HashMap<>();
        param.put("message", "ces");
        template.process(param, out);
        messageHelper.setText(out.toString(), true);
        // 添加附件
        for (File file : files) {
            messageHelper.addAttachment(file.getName(), file);
        }
        javaMailSender.send(message);
    }
    public void sendMail(MailMessage mailMessage)  {
        validateParam(mailMessage);
        try {
            logger.info("邮件发送开始。。。。。。。。。。。");
            // 创建邮件对象
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");
            // 发件人
            messageHelper.setFrom(new InternetAddress(mailOptionProperties.getUsername(), mailOptionProperties.getPersonal(), "UTF-8"));
            //收件人集合
            List<String> recipient = mailMessage.getRecipient();
            if (CollectionUtils.isNotEmpty(recipient)) {
                // 收件人邮箱
                messageHelper.setTo(recipient.toArray(new String[0]));
            }
            //抄送
            String[] cc = mailMessage.getCc();
            if (cc != null && cc.length > 0) {
                messageHelper.setCc(cc);
            }
            //密送(不会在邮件收件人名单中显示出来)
            String[] bcc = mailMessage.getBcc();
            if (bcc != null && bcc.length > 0) {
                messageHelper.setBcc(bcc);
            }
            if (ObjectUtils.isNotEmpty(mailMessage.getSendDate())) {
                //发送时间
                messageHelper.setSentDate(mailMessage.getSendDate());
            }
            //附件
            List<File> fileList = mailMessage.getFileList();
            if (CollectionUtils.isNotEmpty(fileList)) {
                for (File file : fileList) {
                    messageHelper.addAttachment(file.getName(), file);
                }
            }
            List<FileData> fileDataList = mailMessage.getFileDataList();
            if (CollectionUtils.isNotEmpty(fileDataList)) {
                for (FileData fileData : fileDataList) {
                    messageHelper.addAttachment(fileData.getFileName(), new InputStreamResource(fileData.getInputStream()));
                }
            }
            // 邮件的主题
            messageHelper.setSubject(mailMessage.getSubject());
            // 邮件的文本内容,true表示文本以html格式打开
            messageHelper.setText(mailMessage.getText(), mailMessage.isHtml());
            //  发送邮件
            javaMailSender.send(message);
            logger.info("邮件发送结束。。。。。。。。。。。");
        } catch (Exception e) {
            logger.error("邮件发送失败。。。。。。。。。。。",e);
        }
    }
    private void validateParam(MailMessage mailMessage) {
        if (!StringUtils.hasText(mailMessage.getSubject())) {
            throw new Exception("邮件标题不能为空");
        } else if (!StringUtils.hasText(mailMessage.getText())) {
            throw new Exception("邮件内容不能为空");
        } else if (mailMessage.getRecipient().isEmpty()) {
            throw new Exception("收件人不能为空");
        }
    }
}

测试

public class Test {
    protected Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private JavaMailOperation javaMailOperate;
    @Autowired
    private RemoteFilesystemConfiguration configuration;
    @GetMapping("/mail")
     public void test() throws Exception {
        logger.info("邮件发送开始。。。。。。。。。。。");
        String subject = "发送测试";
        String text = "邮件内容。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。";
        File file = new File("C:\\Users\\Desktop\\Stream流常用操作.md");
        InputStreamSource is = new FileSystemResource(file);
        javaMailOperate.sendMail(subject,text,file.getName(),"xxx@qq.com",is);
        logger.info("邮件发送结束。。。。。。。。。。。");
    }
    @GetMapping("/free")
     public void test1() throws Exception {
        String subject = "发送测试";
        String text = "邮件内容。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。";
        File file = new File("C:\\Users\\Desktop\\Stream流常用操作.md");
        javaMailOperate.sendFreemarkerMail(subject,text,"xxx@qq.com", Collections.singletonList(file));
    }
    @GetMapping("/upload")
     public void test2(MultipartFile multipartFile) throws Exception {
        String subject = "发送测试";
        String text = "邮件内容。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。";
        String originalFilename = multipartFile.getOriginalFilename();
        InputStream is = multipartFile.getInputStream();
        InputStreamSource iss = new InputStreamResource(is);
        javaMailOperate.sendMail(subject,text,originalFilename,"xxx@qq.com",iss);
    }
    @GetMapping("/msg")
     public void test3() throws Exception {
        File file = new File("C:\\Users\\tjx\\Desktop\\Stream流常用操作.md");
        MailMessage mailMessage = new MailMessage();
        mailMessage.setRecipient(Collections.singletonList("xxx@qq.com"));
        mailMessage.setCc(new String[]{"xxx@qq.com"});
        mailMessage.setBcc(new String[]{"li5xxxx1@126.com"});
        mailMessage.setSendDate(new Date());
        mailMessage.setSubject("发送测试");
        mailMessage.setText("内容");
        mailMessage.setHtml(true);
        mailMessage.setFileList(Collections.singletonList(file));
        javaMailOperate.sendMail(mailMessage);
    }
    //流文件测试
   @GetMapping("/msg1")
     public void test4(List<String> urlList) throws Exception {
        MailMessage mailMessage = new MailMessage();
        mailMessage.setRecipient(Collections.singletonList("xxx@qq.com"));
        mailMessage.setCc(new String[]{"xxxx@qq.com"});
        mailMessage.setBcc(new String[]{"xxx@126.com"});
        mailMessage.setSendDate(new Date());
        mailMessage.setSubject("发送测试");
        mailMessage.setText("内容");
        FTPClient ftpClient =new FTPClient();
        ByteArrayOutputStream out = null;
        InputStream is = null;
        try {
            ftpClient.connect(configuration.getHost(),configuration.getPort());
            ftpClient.login(configuration.getUsername(), configuration.getPassword());
            ftpClient.setBufferSize(1024);
            // 设置文件类型(二进制)
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            out = new ByteArrayOutputStream();
            for (String url : urlList) {
                if(is!=null){
                    is.reset();
                }
                ftpClient.retrieveFile(url, out);
                is = new ByteArrayInputStream(out.toByteArray());
                List<FileData> fileDataList = Lists.newArrayList();
                fileDataList.add(new FileData(is,"测试1.txt"));
            }
            javaMailOperate.sendMail(mailMessage);
        } catch (Exception e) {
            logger.error("邮件发送失败。。。。。。。。。。。",e);
        }finally {
            ftpClient.logout();
            ftpClient.disconnect();
            if (out != null) {
                out.close();
            }
            if (is != null) {
                is.close();
            }
        }
    }
}

2.方式二:java直接发送

       <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>mail</artifactId>
            <version>1.4.7</version>
        </dependency>
import com.sun.mail.util.MailSSLSocketFactory;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.*;
import javax.mail.internet.*;
import java.io.File;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
public class MailUtils {
	public static void sendMail(String email, String emailMsg, List<File> files)
			throws GeneralSecurityException {
		// 发件人电子邮箱
		String fromAddress = "xxx@qq.com";
		// 指定发送邮件的主机为 smtp.qq.com QQ 邮件服务器
		String host = "smtp.qq.com";
		// 获取系统属性
		Properties properties = System.getProperties();
        //开启认证
		properties.setProperty("mail.smtp.auth", "true");
        //启用调试
		properties.setProperty("mail.debug", "true");
        //设置链接超时
		properties.setProperty("mail.smtp.timeout", "1000");
        //设置端口
		properties.setProperty("mail.smtp.port", "465");
        //设置ssl端口
		properties.setProperty("mail.smtp.socketFactory.port", "465");
		properties.setProperty("mail.smtp.socketFactory.fallback", "false");
		properties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
		// 设置邮件服务器
		properties.setProperty("mail.smtp.host", host);
		properties.put("mail.smtp.auth", "true");
		MailSSLSocketFactory sf = new MailSSLSocketFactory();
		sf.setTrustAllHosts(true);
		properties.put("mail.smtp.ssl.enable", "true");
		properties.put("mail.smtp.ssl.socketFactory", sf);
		// 获取默认session对象
		Session session = Session.getDefaultInstance(properties, new Authenticator() {
			@Override
            public PasswordAuthentication getPasswordAuthentication() {
				return new PasswordAuthentication("xxxx@qq.com", "xxx"); //发件人邮件用户名、密码
			}
		});
		try {
			// 创建默认的 MimeMessage 对象
			MimeMessage message = new MimeMessage(session);
			// Set From: 发件人
			message.setFrom(new InternetAddress(fromAddress,"li","UTF-8"));
			// Set To: 收件人
			message.addRecipient(Message.RecipientType.TO, new InternetAddress(email));
			//设置邮件主题
			message.setSubject("测试","UTF-8");
            message.setText("这是一封激活邮件,请<a href='#'>点击</a>","UTF-8");
            // 设置消息体
			String content = "<h1>邮件发送测试</h1><h3><div style=\"color: red\">" + emailMsg + "</div></h3>";
            Multipart multipart = new MimeMultipart();
            //邮件正文
            BodyPart contentPart = new MimeBodyPart();
            //邮件文字
            contentPart.setContent(content, "text/html;charset=utf-8");
            multipart.addBodyPart(contentPart);
            if (files!=null&&!files.isEmpty()) {
                 //添加附件
                for (File attachment : files) {
                    BodyPart attachmentPart = new MimeBodyPart();
                    DataSource source = new FileDataSource(attachment);
                    attachmentPart.setDataHandler(new DataHandler(source));
                    //避免中文乱码的处理
                    attachmentPart.setFileName(MimeUtility.encodeWord(attachment.getName()));
                    multipart.addBodyPart(attachmentPart);
                }
            }
            message.setContent(multipart);
            message.saveChanges();
			// 发送消息
			Transport.send(message);
			System.out.println("Sent message successfully....");
		} catch (Exception mex) {
			mex.printStackTrace();
		}
	}
	public static void main(String[] args) throws AddressException, MessagingException, GeneralSecurityException {
		MailUtils.sendMail("xxx@qq.com", "abcdefg", Arrays.asList(new File("C:\\Users\\Desktop\\Stream流常用操作.md")));
	}
}

附件名称太长被截断问题,启动类添加

System.getProperties().setProperty("mail.mime.splitlongparameters", "false");

到此这篇关于SpringBoot整合JavaMail邮件的文章就介绍到这了,更多相关SpringBoot整合JavaMail邮件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一篇文章搞定Struts2的类型转换

    一篇文章搞定Struts2的类型转换

    这篇文章主要介绍了关于Struts2类型转换的相关资料,文中主要介绍了Struts2的类型转换器和自定义类型转换器的实现,有需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-02-02
  • Java经典设计模式之责任链模式原理与用法详解

    Java经典设计模式之责任链模式原理与用法详解

    这篇文章主要介绍了Java经典设计模式之责任链模式,简单说明了责任链模式的概念、原理,并结合实例形式分析了java实现责任链模式的具体用法与相关注意事项,需要的朋友可以参考下
    2017-08-08
  • Java MapStruct解了对象映射的毒

    Java MapStruct解了对象映射的毒

    这篇文章主要介绍了MapStruct解了对象映射的毒,对MapStruct感兴趣的同学,可以参考下
    2021-04-04
  • Spring项目运行依赖spring-contex解析

    Spring项目运行依赖spring-contex解析

    这篇文章主要介绍了Spring项目运行依赖spring-contex解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • Java中字节流和字符流的理解(超精简!)

    Java中字节流和字符流的理解(超精简!)

    Java通过称为流的抽象来执行I/O操作,下面这篇文章主要给大家介绍了关于Java中字节流和字符流理解,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • SpringBoot启动器Starters使用及原理解析

    SpringBoot启动器Starters使用及原理解析

    这篇文章主要介绍了SpringBoot启动器Starters使用及原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • Java之api网关断言及过滤器案例讲解

    Java之api网关断言及过滤器案例讲解

    这篇文章主要介绍了Java之api网关断言及过滤器案例讲解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • 教你如何使用Java多线程编程LockSupport工具类

    教你如何使用Java多线程编程LockSupport工具类

    在Java工具包中有一个LockSupport工具类,主要负责挂起和唤醒线程,这篇文章主要介绍了教你如何使用Java多线程编程LockSupport工具类,需要的朋友可以参考下
    2021-04-04
  • java递归实现汉诺塔步骤介绍

    java递归实现汉诺塔步骤介绍

    大家好,本篇文章主要讲的是java递归实现汉诺塔步骤介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2022-01-01
  • Java多线程中的互斥锁解析

    Java多线程中的互斥锁解析

    这篇文章主要介绍了Java多线程中的互斥锁解析,Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性,每个对象都对应于一个可称为互斥锁的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象,需要的朋友可以参考下
    2023-09-09

最新评论