百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

干掉 BeanUtils!试试这款 Bean 自动映射工具,真心强大

yuyutoo 2024-10-29 17:29 1 浏览 0 评论

平时做项目的时候,经常需要做PO、VO、DTO之间的转换。简单的对象转换,使用BeanUtils基本上是够了,但是复杂的转换,如果使用它的话又得写一堆Getter、Setter方法了。今天给大家推荐一款对象自动映射工具MapStruct,功能真心强大!

关于BeanUtils

平时我经常使用Hutool中的BeanUtil类来实现对象转换,用多了之后就发现有些缺点:

  • 对象属性映射使用反射来实现,性能比较低;
  • 对于不同名称或不同类型的属性无法转换,还得单独写Getter、Setter方法;
  • 对于嵌套的子对象也需要转换的情况,也得自行处理;
  • 集合对象转换时,得使用循环,一个个拷贝。

对于这些不足,MapStruct都能解决,不愧为一款功能强大的对象映射工具!

MapStruct简介

MapStruct是一款基于Java注解的对象属性映射工具,在Github上已经有4.5K+Star。使用的时候我们只要在接口中定义好对象属性映射规则,它就能自动生成映射实现类,不使用反射,性能优秀,能实现各种复杂映射。

IDEA插件支持

作为一款非常流行的对象映射工具,MapStruct还提供了专门的IDEA插件,我们在使用之前可以先安装好插件。

项目集成

在SpingBoot中集成MapStruct非常简单,仅续添加如下两个依赖即可,这里使用的是1.4.2.Final版本。

<dependency>
    <!--MapStruct相关依赖-->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>${mapstruct.version}</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

基本使用

集成完MapStruct之后,我们来体验下它的功能吧,看看它有何神奇之处!

基本映射

我们先来个快速入门,体验一下MapStruct的基本功能,并聊聊它的实现原理。

  • 首先我们准备好要使用的会员PO对象Member
/**
 * 购物会员
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    private Date birthday;
    private String phone;
    private String icon;
    private Integer gender;
}
  • 然后再准备好会员的DTO对象MemberDto,我们需要将Member对象转换为MemberDto对象;
/**
 * 购物会员Dto
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberDto {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    //与PO类型不同的属性
    private String birthday;
    //与PO名称不同的属性
    private String phoneNumber;
    private String icon;
    private Integer gender;
}
  • 然后创建一个映射接口MemberMapper,实现同名同类型属性、不同名称属性、不同类型属性的映射;
/**
 * 会员对象映射
 * Created by macro on 2021/10/21.
 */
@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "phone",target = "phoneNumber")
    @Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
    MemberDto toDto(Member member);
}
  • 接下来在Controller中创建测试接口,直接通过接口中的INSTANCE实例调用转换方法toDto
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "基本映射")
    @GetMapping("/baseMapping")
    public CommonResult baseTest() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        MemberDto memberDto = MemberMapper.INSTANCE.toDto(memberList.get(0));
        return CommonResult.success(memberDto);
    }
}
  • 运行项目后在Swagger中测试接口,发现PO所有属性已经成功转换到DTO中去了,Swagger访问地址:http://localhost:8088/swagger-ui
  • 其实MapStruct的实现原理很简单,就是根据我们在Mapper接口中使用的@Mapper@Mapping等注解,在运行时生成接口的实现类,我们可以打开项目的target目录看下;
  • 下面是MapStruct为MemberMapper生成好的对象映射代码,可以和手写Getter、Setter说再见了!
public class MemberMapperImpl implements MemberMapper {
    public MemberMapperImpl() {
    }

    public MemberDto toDto(Member member) {
        if (member == null) {
            return null;
        } else {
            MemberDto memberDto = new MemberDto();
            memberDto.setPhoneNumber(member.getPhone());
            if (member.getBirthday() != null) {
                memberDto.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(member.getBirthday()));
            }

            memberDto.setId(member.getId());
            memberDto.setUsername(member.getUsername());
            memberDto.setPassword(member.getPassword());
            memberDto.setNickname(member.getNickname());
            memberDto.setIcon(member.getIcon());
            memberDto.setGender(member.getGender());
            return memberDto;
        }
    }
}

集合映射

MapStruct也提供了集合映射的功能,可以直接将一个PO列表转换为一个DTO列表,再也不用一个个对象转换了!

  • MemberMapper接口中添加toDtoList方法用于列表转换;
/**
 * 会员对象映射
 * Created by macro on 2021/10/21.
 */
@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "phone",target = "phoneNumber")
    @Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
    List<MemberDto> toDtoList(List<Member> list);
}
  • 在Controller中创建测试接口,直接通过Mapper接口中的INSTANCE实例调用转换方法toDtoList
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {

    @ApiOperation(value = "集合映射")
    @GetMapping("/collectionMapping")
    public CommonResult collectionMapping() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        List<MemberDto> memberDtoList = MemberMapper.INSTANCE.toDtoList(memberList);
        return CommonResult.success(memberDtoList);
    }
}
  • 在Swagger中调用接口测试下,PO列表已经转换为DTO列表了。

子对象映射

MapStruct对于对象中包含子对象也需要转换的情况也是有所支持的。

  • 例如我们有一个订单PO对象Order,嵌套有MemberProduct对象;
/**
 * 订单
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
    private Long id;
    private String orderSn;
    private Date createTime;
    private String receiverAddress;
    private Member member;
    private List<Product> productList;
}
  • 我们需要转换为OrderDto对象,OrderDto中包含MemberDtoProductDto两个子对象同样需要转换;
/**
 * 订单Dto
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class OrderDto {
    private Long id;
    private String orderSn;
    private Date createTime;
    private String receiverAddress;
    //子对象映射Dto
    private MemberDto memberDto;
    //子对象数组映射Dto
    private List<ProductDto> productDtoList;
}
  • 我们只需要创建一个Mapper接口,然后通过使用uses将子对象的转换Mapper注入进来,然后通过@Mapping设置好属性映射规则即可;
/**
 * 订单对象映射
 * Created by macro on 2021/10/21.
 */
@Mapper(uses = {MemberMapper.class,ProductMapper.class})
public interface OrderMapper {
    OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);

    @Mapping(source = "member",target = "memberDto")
    @Mapping(source = "productList",target = "productDtoList")
    OrderDto toDto(Order order);
}
  • 接下来在Controller中创建测试接口,直接通过Mapper中的INSTANCE实例调用转换方法toDto
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    
    @ApiOperation(value = "子对象映射")
    @GetMapping("/subMapping")
    public CommonResult subMapping() {
        List<Order> orderList = getOrderList();
        OrderDto orderDto = OrderMapper.INSTANCE.toDto(orderList.get(0));
        return CommonResult.success(orderDto);
    }
}
  • 在Swagger中调用接口测试下,可以发现子对象属性已经被转换了。

合并映射

MapStruct也支持把多个对象属性映射到一个对象中去。

  • 例如这里把MemberOrder的部分属性映射到MemberOrderDto中去;
/**
 * 会员商品信息组合Dto
 * Created by macro on 2021/10/21.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class MemberOrderDto extends MemberDto{
    private String orderSn;
    private String receiverAddress;
}
  • 然后在Mapper中添加toMemberOrderDto方法,这里需要注意的是由于参数中具有两个属性,需要通过参数名称.属性的名称来指定source来防止冲突(这两个参数中都有id属性);
/**
 * 会员对象映射
 * Created by macro on 2021/10/21.
 */
@Mapper
public interface MemberMapper {
    MemberMapper INSTANCE = Mappers.getMapper(MemberMapper.class);

    @Mapping(source = "member.phone",target = "phoneNumber")
    @Mapping(source = "member.birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
    @Mapping(source = "member.id",target = "id")
    @Mapping(source = "order.orderSn", target = "orderSn")
    @Mapping(source = "order.receiverAddress", target = "receiverAddress")
    MemberOrderDto toMemberOrderDto(Member member, Order order);
}
  • 接下来在Controller中创建测试接口,直接通过Mapper中的INSTANCE实例调用转换方法toMemberOrderDto
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    
    @ApiOperation(value = "组合映射")
    @GetMapping("/compositeMapping")
    public CommonResult compositeMapping() {
        List<Order> orderList = LocalJsonUtil.getListFromJson("json/orders.json", Order.class);
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        Member member = memberList.get(0);
        Order order = orderList.get(0);
        MemberOrderDto memberOrderDto = MemberMapper.INSTANCE.toMemberOrderDto(member,order);
        return CommonResult.success(memberOrderDto);
    }
}
  • 在Swagger中调用接口测试下,可以发现Member和Order中的属性已经被映射到MemberOrderDto中去了。

进阶使用

通过上面的基本使用,大家已经可以玩转MapStruct了,下面我们再来介绍一些进阶的用法。

使用依赖注入

上面我们都是通过Mapper接口中的INSTANCE实例来调用方法的,在Spring中我们也是可以使用依赖注入的。

  • 想要使用依赖注入,我们只要将@Mapper注解的componentModel参数设置为spring即可,这样在生成接口实现类时,MapperStruct会为其添加@Component注解;
/**
 * 会员对象映射(依赖注入)
 * Created by macro on 2021/10/21.
 */
@Mapper(componentModel = "spring")
public interface MemberSpringMapper {
    @Mapping(source = "phone",target = "phoneNumber")
    @Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")
    MemberDto toDto(Member member);
}
  • 接下来在Controller中使用@Autowired注解注入即可使用;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {

    @Autowired
    private MemberSpringMapper memberSpringMapper;

    @ApiOperation(value = "使用依赖注入")
    @GetMapping("/springMapping")
    public CommonResult springMapping() {
        List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
        MemberDto memberDto = memberSpringMapper.toDto(memberList.get(0));
        return CommonResult.success(memberDto);
    }
}
  • 在Swagger中调用接口测试下,可以发现与之前一样可以正常使用。

使用常量、默认值和表达式

使用MapStruct映射属性时,我们可以设置属性为常量或者默认值,也可以通过Java中的方法编写表达式来自动生成属性。

  • 例如下面这个商品类Product对象;
/**
 * 商品
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
    private Long id;
    private String productSn;
    private String name;
    private String subTitle;
    private String brandName;
    private BigDecimal price;
    private Integer count;
    private Date createTime;
}
  • 我们想把Product转换为ProductDto对象,id属性设置为常量,count设置默认值为1,productSn设置为UUID生成;
/**
 * 商品Dto
 * Created by macro on 2021/10/12.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ProductDto {
    //使用常量
    private Long id;
    //使用表达式生成属性
    private String productSn;
    private String name;
    private String subTitle;
    private String brandName;
    private BigDecimal price;
    //使用默认值
    private Integer count;
    private Date createTime;
}
  • 创建ProductMapper接口,通过@Mapping注解中的constantdefaultValueexpression设置好映射规则;
/**
 * 商品对象映射
 * Created by macro on 2021/10/21.
 */
@Mapper(imports = {UUID.class})
public interface ProductMapper {
    ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class);

    @Mapping(target = "id",constant = "-1L")
    @Mapping(source = "count",target = "count",defaultValue = "1")
    @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
    ProductDto toDto(Product product);
}
  • 接下来在Controller中创建测试接口,直接通过接口中的INSTANCE实例调用转换方法toDto
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    @ApiOperation(value = "使用常量、默认值和表达式")
    @GetMapping("/defaultMapping")
    public CommonResult defaultMapping() {
        List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
        Product product = productList.get(0);
        product.setId(100L);
        product.setCount(null);
        ProductDto productDto = ProductMapper.INSTANCE.toDto(product);
        return CommonResult.success(productDto);
    }
}
  • 在Swagger中调用接口测试下,对象已经成功转换。

在映射前后进行自定义处理

MapStruct也支持在映射前后做一些自定义操作,类似AOP中的切面。

  • 由于此时我们需要创建自定义处理方法,创建一个抽象类ProductRoundMapper,通过@BeforeMapping注解自定义映射前操作,通过@AfterMapping注解自定义映射后操作;
/**
 * 商品对象映射(自定义处理)
 * Created by macro on 2021/10/21.
 */
@Mapper(imports = {UUID.class})
public abstract class ProductRoundMapper {
    public static ProductRoundMapper INSTANCE = Mappers.getMapper(ProductRoundMapper.class);

    @Mapping(target = "id",constant = "-1L")
    @Mapping(source = "count",target = "count",defaultValue = "1")
    @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
    public abstract ProductDto toDto(Product product);

    @BeforeMapping
    public void beforeMapping(Product product){
        //映射前当price<0时设置为0
        if(product.getPrice().compareTo(BigDecimal.ZERO)<0){
            product.setPrice(BigDecimal.ZERO);
        }
    }

    @AfterMapping
    public void afterMapping(@MappingTarget ProductDto productDto){
        //映射后设置当前时间为createTime
        productDto.setCreateTime(new Date());
    }
}
  • 接下来在Controller中创建测试接口,直接通过Mapper中的INSTANCE实例调用转换方法toDto
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    
    @ApiOperation(value = "在映射前后进行自定义处理")
    @GetMapping("/customRoundMapping")
    public CommonResult customRoundMapping() {
        List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
        Product product = productList.get(0);
        product.setPrice(new BigDecimal(-1));
        ProductDto productDto = ProductRoundMapper.INSTANCE.toDto(product);
        return CommonResult.success(productDto);
    }
}
  • 在Swagger中调用接口测试下,可以发现已经应用了自定义操作。

处理映射异常

代码运行难免会出现异常,MapStruct也支持处理映射异常。

  • 我们需要先创建一个自定义异常类;
/**
 * 商品验证异常类
 * Created by macro on 2021/10/22.
 */
public class ProductValidatorException extends Exception{
    public ProductValidatorException(String message) {
        super(message);
    }
}
  • 然后创建一个验证类,当price设置小于0时抛出我们自定义的异常;
/**
 * 商品验证异常处理器
 * Created by macro on 2021/10/22.
 */
public class ProductValidator {
    public BigDecimal validatePrice(BigDecimal price) throws ProductValidatorException {
        if(price.compareTo(BigDecimal.ZERO)<0){
            throw new ProductValidatorException("价格不能小于0!");
        }
        return price;
    }
}
  • 之后我们通过@Mapper注解的uses属性运用验证类;
/**
 * 商品对象映射(处理映射异常)
 * Created by macro on 2021/10/21.
 */
@Mapper(uses = {ProductValidator.class},imports = {UUID.class})
public interface ProductExceptionMapper {
    ProductExceptionMapper INSTANCE = Mappers.getMapper(ProductExceptionMapper.class);

    @Mapping(target = "id",constant = "-1L")
    @Mapping(source = "count",target = "count",defaultValue = "1")
    @Mapping(target = "productSn",expression = "java(UUID.randomUUID().toString())")
    ProductDto toDto(Product product) throws ProductValidatorException;
}
  • 然后在Controller中添加测试接口,设置price-1,此时在进行映射时会抛出异常;
/**
 * MapStruct对象转换测试Controller
 * Created by macro on 2021/10/21.
 */
@RestController
@Api(tags = "MapStructController", description = "MapStruct对象转换测试")
@RequestMapping("/mapStruct")
public class MapStructController {
    @ApiOperation(value = "处理映射异常")
    @GetMapping("/exceptionMapping")
    public CommonResult exceptionMapping() {
        List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
        Product product = productList.get(0);
        product.setPrice(new BigDecimal(-1));
        ProductDto productDto = null;
        try {
            productDto = ProductExceptionMapper.INSTANCE.toDto(product);
        } catch (ProductValidatorException e) {
            e.printStackTrace();
        }
        return CommonResult.success(productDto);
    }
}
  • 在Swagger中调用接口测试下,发现运行日志中已经打印了自定义异常信息。

总结

通过上面对MapStruct的使用体验,我们可以发现MapStruct远比BeanUtils要强大。当我们想实现比较复杂的对象映射时,通过它可以省去写Getter、Setter方法的过程。当然上面只是介绍了MapStruct的一些常用功能,它的功能远不止于此,感兴趣的朋友可以查看下官方文档。

项目源码地址

https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-mapstruct

来源:https://mp.weixin.qq.com/s/Yt3IIBwbgoweQnI0buSW6Q

作者:梦想de星空

相关推荐

MySQL5.5+配置主从同步并结合ThinkPHP5设置分布式数据库

前言:本文章是在同处局域网内的两台windows电脑,且MySQL是5.5以上版本下进行的一主多从同步配置,并且使用的是集成环境工具PHPStudy为例。最后就是ThinkPHP5的分布式的连接,读写...

thinkphp5多语言怎么切换(thinkphp5.1视频教程)

thinkphp5多语言进行切换的步骤:第一步,在配置文件中开启多语言配置。第二步,创建多语言目录。相关推荐:《ThinkPHP教程》第三步,编写语言包。视图代码:控制器代码:效果如下:以上就是thi...

基于 ThinkPHP5 + Bootstrap 的后台开发框架 FastAdmin

FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架。主要特性基于Auth验证的权限管理系统支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置支持单...

Thinkphp5.0 框架实现控制器向视图view赋值及视图view取值操作示

本文实例讲述了Thinkphp5.0框架实现控制器向视图view赋值及视图view取值操作。分享给大家供大家参考,具体如下:Thinkphp5.0控制器向视图view的赋值方式一(使用fetch()方...

thinkphp5实现简单评论回复功能(php评论回复功能源码下载)

由于之前写评论回复都是使用第三方插件:畅言所以也就没什么动手,现在证号在开发一个小的项目,所以就自己动手写评论回复,没写过还真不知道评论回复功能听着简单,但仔细研究起来却无法自拔,由于用户量少,所以...

ThinkPHP框架——实现定时任务,定时更新、清理数据

大家好,我是小蜗牛,今天给大家分享一下,如何用ThinkPHP5.1.*版本实现定时任务,例如凌晨12点更新数据、每隔10秒检测过期会员、每隔几分钟发送请求保证ip的活性等本次分享,主要用到一个名为E...

BeyongCms系统基于ThinkPHP5.1框架的轻量级内容管理系统

BeyongCms内容管理系统(简称BeyongCms)BeyongCms系统基于ThinkPHP5.1框架的轻量级内容管理系统,适用于企业Cms,个人站长等,针对移动App、小程序优化;提供完善简...

YimaoAdminv3企业建站系统,使用 thinkphp5.1.27 + mysql 开发

介绍YimaoAdminv3.0.0企业建站系统,使用thinkphp5.1.27+mysql开发。php要求5.6以上版本,推荐使用5.6,7.0,7.1,扩展(curl,...

ThinkAdmin-V5开发笔记(thinkpad做开发)

前言为了快速开发一款小程序管理后台,在众多的php开源后台中,最终选择了基于thinkphp5的,轻量级的thinkadmin系统,进行二次开发。该系统支持php7。文档地址ThinkAdmin-V5...

thinkphp5.0.9预处理导致的sql注入复现与详细分析

复现先搭建thinkphp5.0.9环境...

thinkphp5出现500错误怎么办(thinkphp页面错误)

thinkphp5出现500错误,如下图所示:相关推荐:《ThinkPHP教程》require():open_basedirrestrictionineffect.File(/home/ww...

Thinkphp5.0极速搭建restful风格接口层

下面是基于ThinkPHPV5.0RC4框架,以restful风格完成的新闻查询(get)、新闻增加(post)、新闻修改(put)、新闻删除(delete)等server接口层。1、下载Thin...

基于ThinkPHP5.1.34 LTS开发的快速开发框架DolphinPHP

DophinPHP(海豚PHP)是一个基于ThinkPHP5.1.34LTS开发的一套开源PHP快速开发框架,DophinPHP秉承极简、极速、极致的开发理念,为开发集成了基于数据-角色的权限管理机...

ThinkPHP5.*远程代码执行高危漏洞手工与升级修复解决方法

漏洞描述由于ThinkPHP5框架对控制器名没有进行足够的安全检测,导致在没有开启强制路由的情况下,黑客构造特定的请求,可直接GetWebShell。漏洞评级严重影响版本ThinkPHP5.0系列...

Thinkphp5代码执行学习(thinkphp 教程)

Thinkphp5代码执行学习缓存类RCE版本5.0.0<=ThinkPHP5<=5.0.10Tp框架搭建环境搭建测试payload...

取消回复欢迎 发表评论: