java 切面_实用|AOP切面编程手段大汇总

c6a23ee22cb7b62d57397ffc4aa14c7b.gif

点击上方"欧学长的架构成长之路" 关注我

前言

首先说一下什么是AOP?

        AOP就是面向切面编程,它是一个思想,通过切面,我们可以将那些反复出现的代码抽取出来,放在一个地方统一处理,提高代码的复用性。AOP的一大好处就是解耦。以下几种方式实现AOP:

1自定义注解+@Aspect

2拦截器

3过滤器

4.JDK动态代理和CGlib

5.设计模型--静态代理

*.基于非侵入式运行时AOP方案(篇幅问题,不细说,感兴趣的朋友可以自行百度阿里开源的jvm-Sandbox)

自定义注解+@Aspect 实现日志记录

1.首先你需要先引入pom依赖。(springboot2.x默认使用的代理是cglib代理)

<dependency>    <groupId>org.springframework.bootgroupId>    <artifactId>spring-boot-starter-aopartifactId>dependency><dependency>    <groupId>com.google.code.gsongroupId>    <artifactId>gsonartifactId>    <version>2.8.5version>dependency>

注意: 

  •     在application.properties中也不需要添加spring.aop.auto=true,这个默认就是true,值为true就是启用@EnableAspectJAutoProxy注解了。 

  •    你不需要手工添加在启动类上添加 @EnableAspectJAutoProxy 注解。 

  •   当你需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,这个默认值是false,不然默认使用的是标准Java的实现(JDK动态代理基于接口代理)。

    2.自定义日志注解(使用Java元注解,Java5.0定义了4个标准的meta-annotation类型)

@Retention(RetentionPolicy.RUNTIME) //定义为运行时使用注解@Target({ElementType.METHOD})//在方法上使用注解@Documented//注解将包含javaDoc中public @interface WebLog {    /**     * 日志描述信息     * @return     */     //定义一个属性,默认作为空字符串    String description() default "";}

3.配置AOP切面类

@Aspect@Component //将这个类交给Spring管理public class WebLogAspect {    private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);    /** 换行符 */    private static final String LINE_SEPARATOR = System.lineSeparator();    /** 以自定义 @WebLog 注解为切点 */    @Pointcut("@annotation(site.exception.aspect.WebLog)") //<=全路径    public void webLog() {}    /**     * 在切点之前织入     * @param joinPoint     * @throws Throwable     */    @Before("webLog()")    public void doBefore(JoinPoint joinPoint) throws Throwable {        // 开始打印请求日志        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();        HttpServletRequest request = attributes.getRequest();        // 获取 @WebLog 注解的描述信息        String methodDescription = getAspectLogDescription(joinPoint);        // 打印请求相关参数        logger.info("========================================== Start ==========================================");        // 打印请求 url        logger.info("URL: {}", request.getRequestURL().toString());        // 打印描述信息        logger.info("Description : {}", methodDescription);        // 打印 Http method        logger.info("HTTP Method : {}", request.getMethod());        // 打印调用 controller 的全路径以及执行方法        logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());        // 打印请求的 IP        logger.info("IP : {}", request.getRemoteAddr());        // 打印请求入参        logger.info("Request Args : {}", new Gson().toJson(joinPoint.getArgs()));    }    /**     * 在切点之后织入     * @throws Throwable     */    @After("webLog()")    public void doAfter() throws Throwable {        // 接口结束后换行,方便分割查看        logger.info("=========================================== End ===========================================" + LINE_SEPARATOR);    }    /**     * 环绕     * @param proceedingJoinPoint     * @return     * @throws Throwable     */    @Around("webLog()")    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {        long startTime = System.currentTimeMillis();       //执行切点后,会去依次调用 @Before -> 接口逻辑代码(之后,执行完doAround方法) -> @After -> @AfterReturning;        Object result = proceedingJoinPoint.proceed();        // 打印出参        logger.info("Response Args  : {}", new Gson().toJson(result));        // 执行耗时        logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);        return result;    }    /**     * 获取切面注解的描述     *     * @param joinPoint 切点     * @return 描述信息     * @throws Exception     */    public String getAspectLogDescription(JoinPoint joinPoint)            throws Exception {        String targetName = joinPoint.getTarget().getClass().getName();        String methodName = joinPoint.getSignature().getName();        Object[] arguments = joinPoint.getArgs();        Class targetClass = Class.forName(targetName);        Method[] methods = targetClass.getMethods();        StringBuilder description = new StringBuilder("");        for (Method method : methods) {            if (method.getName().equals(methodName)) {                Class[] clazzs = method.getParameterTypes();                if (clazzs.length == arguments.length) {                    description.append(method.getAnnotation(WebLog.class).description());                    break;                }            }        }        return description.toString();    }}

4.使用注解

@PostMapping("/user")@WebLog(description="用户请求接口")public User userLogin(@RequestBody User user){  logger.info("user login ...");  return user; }

特别说明

d55161d23e7307445dd361ad50d87250.png

多切面如何指定优先级?

假设说我们的服务中不止定义了一个切面,比如说我们针对 Web 层的接口,不止要打印日志,还要校验 token 等。要如何指定切面的优先级呢?也就是如何指定切面的执行顺序?

我们可以通过 @Order(i)注解来指定优先级,注意:i 值越小,优先级则越高。

假设说我们定义上面这个日志切面的优先级为 @Order(10), 然后我们还有个校验 token 的切面 CheckTokenAspect.java,我们定义为了 @Order(11), 那么它们之间的执行顺序如下

a68365db00910c8b6441965e6e74c155.png

         spring借鉴了AspectJ的切面,以提供注解驱动的AOP,本质上它依然是Spring基于代理的AOP,只是编程模型与AspectJ完全一致,这种风格的好处就是不需要使用XML进行配置。

通过拦截器实现

拦截器拦截的是URL

拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。Spring中拦截器有三个方法:preHandle,postHandle,afterCompletion。

@Configurationpublic class HomeOpenHandlerConfigration extends WebMvcConfigurerAdapter {    //关键,将拦截器作为bean写入配置中    @Bean    public HomeOpenInterceptor myInterceptor(){        return new HomeOpenInterceptor();    }        @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(myInterceptor()).addPathPatterns("/api/open/portal/**")        .excludePathPatterns("/api/open/footerInfo").excludePathPatterns("/api/open/portal/template/default");        super.addInterceptors(registry);    }}
/** * 首页外放拦截器 * */@Componentpublic class HomeOpenInterceptor extends HandlerInterceptorAdapter {        @Autowired    private PortalCommonService portalCommonService;    @Autowired    private ApplicationProperties applicationProperties;        @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)            throws Exception {        //判断是否需要拦截        Boolean flag = false;        if(flag){            //判断是否允许不登录的情况下 访问主页            //如果不允许匿名访问返回401               throw new UnauthenticatedException();        }        //否则允许直接放过,不进行任何拦截        return true;    }}

过滤器的实现

过滤器拦截的是URL

Spring中自定义过滤器(Filter)一般只有一个方法,返回值是void,当请求到达web容器时,会探测当前请求地址是否配置有过滤器,有则调用该过滤器的方法(可能会有多个过滤器),然后才调用真实的业务逻辑,至此过滤器任务完成。过滤器并没有定义业务逻辑执行前、后等,仅仅是请求到达就执行。

特别注意:过滤器方法的入参有request,response,FilterChain,其中FilterChain是过滤器链,使用比较简单,而request,response则关联到请求流程,因此可以对请求参数做过滤和修改,同时FilterChain过滤链执行完,并且完成业务流程后,会返回到过滤器,此时也可以对请求的返回数据做处理。

@Component@Order(1) //注解,配合 @WebFilter 注解使用,用于多个过滤器时定义执行顺序,值越小越先执行。@WebFilter(urlPatterns = "/*", filterName = "test")public class TestFilter implements Filter {   @Override  public void init(FilterConfig arg0) throws ServletException {    System.out.println("过滤器初始化");  }   @Override  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)      throws IOException, ServletException {    System.out.printf("过滤器实现");    System.out.println(((HttpServletRequest) servletRequest).getRequestURI());    filterChain.doFilter(servletRequest, servletResponse);  }   @Override  public void destroy() {    System.out.println("过滤器销毁了");  } }

3.JDK动态代理 

@SuppressWarnings("restriction")public class JavaProxyTest {    public static void main(String[] args) throws Exception {        JavaProxyInterface javaProxyInterface = new ConcreteClass();                JavaProxyInterface newJavaProxyInterface = (JavaProxyInterface) Proxy.newProxyInstance(                JavaProxyTest.class.getClassLoader(), new Class[] { JavaProxyInterface.class },                new MyInvocationHandler(javaProxyInterface));        //这里可以看到这个类以及被代理,在执行方法前会执行aopMethod()。这里需要注意的是oneDay()方法和oneDayFinal()的区别。oneDayFinal的方法aopMethod执行1次,oneDay的aopMethod执行1次        newJavaProxyInterface.gotoSchool();        newJavaProxyInterface.gotoWork();        newJavaProxyInterface.oneDayFinal();        newJavaProxyInterface.oneDay();    }}/*** InvocationHandler 的一个实现,实际上处理代理的逻辑在这里*/class MyInvocationHandler implements InvocationHandler {    JavaProxyInterface javaProxy;    public MyInvocationHandler(JavaProxyInterface javaProxy) {        this.javaProxy = javaProxy;    }    private void aopMethod() {        System.out.println("before method");    }  //继承方法,代理时实际执行的犯法,如果要实现原方法,则需要调用method.invoke(javaProxy, args),这里还调用了一个aopMethod(),可以类比于Spring中的切面before注解。    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        aopMethod();        return method.invoke(javaProxy, args);    }}/*** 需要一个最顶层接口,必须*/interface JavaProxyInterface {    void gotoSchool();    void gotoWork();    void oneDay();    void oneDayFinal();}/*** 需要被代理的类,实现了顶层接口,非必须*/class ConcreteClass implements JavaProxyInterface {    @Override    public void gotoSchool() {        System.out.println("gotoSchool");    }    @Override    public void gotoWork() {        System.out.println("gotoWork");    }    @Override    public void oneDay() {        gotoSchool();        gotoWork();    }    @Override    public final void oneDayFinal() {        gotoSchool();        gotoWork();    }}

底层实现部分代码:

// proxyName 为类名,interfaces为顶层接口Classbyte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); File file = new File("D:/testProxy/Ddd.class");FileOutputStream fileOutputStream = new FileOutputStream(file);fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();

CGlib动态代理

public class CglibProxyTest {    public static void main(String[] args) throws Exception {        CglibTestSon CglibTestSon = new CglibTestSon();        Enhancer enhancer = new Enhancer();        Callback s = new MthdInvoker(CglibTestSon);        enhancer.setSuperclass(CglibTestSon.class);        Callback callbacks[] = new Callback[] { s };        enhancer.setCallbacks(callbacks);        CglibTestSon CglibTestSon2 = (CglibTestSon) enhancer.create();        CglibTestSon2.gotoHome();        CglibTestSon2.gotoSchool();    //这里可以看到这个类以及被代理,在执行方法前会执行aopMethod()。这里需要注意的是oneDay()方法和onedayFinal()的区别。onedayFinal的方法aopMethod执行2次,oneDay的aopMethod执行1次 ,注意这里和jdk的代理的区别        CglibTestSon2.oneday();        CglibTestSon2.onedayFinal();    }}/*** 需要被代理的类,不需要实现顶层接口*/class CglibTestSon {    public CglibTestSon() {    }    public void gotoHome() {        System.out.println("============gotoHome============");    }    public void gotoSchool() {        System.out.println("===========gotoSchool============");    }    public void oneday() {        gotoHome();        gotoSchool();    }    public final void onedayFinal() {        gotoHome();        gotoSchool();    }}/*** 可以类比于jdk动态代理中的InvocationHandler ,实际上被代理后重要的类,实际上后续执行的就是intercept里的方法,如果需要执行原来的方法,则调用 method.invoke(s, args);这里也加了一个aopMethod();*/class MthdInvoker implements MethodInterceptor {    private CglibTestSon s;    public MthdInvoker(CglibTestSon s) {        this.s = s;    }    private void aopMethod() {        System.out.println("i am aopMethod");    }    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {        aopMethod();        Object a = method.invoke(s, args);        return a;    }}

CGlib底层实现部分代码:

byte[] bs = DefaultGeneratorStrategy.INSTANCE.generate(enhancer);FileOutputStream fileOutputStream = new FileOutputStream("D:/testProxy/Cc.class");fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();

    简单来看就是先生成新的class文件,然后加载到jvm中,然后使用反射,先用class取得他的构造方法,然后使用构造方法反射得到他的一个实例。

标红的是最复杂的。然后cglib的实现原理基本一致,唯一的区别在于生成新的class文件方式和结果不一样。

4.静态代理模式的实现AOP

Font.java

package com.java.proxy; import lombok.Data; @Datapublic class Font {   private String name;}

FontProvider.java

package com.java.proxy;  public interface FontProvider {    Font getFont(String name);    void printName(String name);}

代理类CachedFontProvider.java

/** * 给FontProvider的getFont添加缓存功能,用静态代理来实现 * */public class CachedFontProvider implements FontProvider {   private FontProvider fontProvider;    private Map cached = new HashMap();      public CachedFontProvider() {    fontProvider = new FontProviderFromDisk();  }     @Override  public Font getFont(String name) {    System.out.println("静态代理getFont()");    Font font = cached.get(name);    if(font == null) {      font = fontProvider.getFont(name);      cached.put(name, font);    }    return font;  }     @Override  public void printName(String name) {    System.out.println("静态代理printName()");    fontProvider.printName(name);;      }   }

工厂类ProviderFactory.java

/** * 每个字体都增加了缓存功能,其实工厂就是用的缓存字体提供器,跟io一样 * 使用代理(静态),已经避免了再去修改每个字体提供器(这违反了开闭原则,而且工作量很大,容易出错;而且如果要增加别的功能 * 比如日志打印,权限检查,异常处理,每个都要去修改,代码重复,而且很麻烦) *  * ② 然而为什么要用动态代理? *考虑以下各种情况,有多个提供类,每个类都有getXxx(String name)方法, *每个类都要加入缓存功能,使用静态代理虽然也能实现,但是也是略显繁琐,需要手动一一创建代理类。 */public class ProviderFactory {   public static FontProvider getFontProvider() {    return new CachedFontProvider();  }  }

测试类;

public class Business {  public static void main(String[] args) {     FontProvider fontProvider = ProviderFactory.getFontProvider();          Font font = fontProvider.getFont("微软雅黑");          System.out.println(font);          fontProvider.printName("代理模式实现AOP");  } }

总结

        三者功能类似,但各有优势,从过滤器--》拦截器--》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。

点个“在看”表示朕

已阅

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/551664.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php可以控制硬件吗,控制面板的作用是设置硬件接口吗?

错误&#xff0c;控制面板的作用是对系统进行有关的设置。控制面板是一个系统文件夹&#xff0c;用来提供各种对计算机系统进行设置和管理的工具&#xff1b;使用控制面板可以对系统进行设置与管理&#xff0c;例如设置系统环境参数的默认值和属性&#xff0c;添加新的应用程序…

python dict保存到文件_将dict写入txt文件并将其读回?

我正试着把字典写成一个txt文件。然后用raw_input键入键来读取dict值。我觉得我只是错过了一步&#xff0c;但我已经找了一段时间了。我知道这个错误File "name.py", line 24, in readingprint whip[name]TypeError: string indices must be integers, not str我的代…

不知道工作组名称怎样加入_剩米饭不知道怎样做?试试泡菜炒饭,再也不用担心米饭做多了...

剩米饭不知道怎样做&#xff1f;试试泡菜炒饭&#xff0c;再也不用担心米饭做多了东北的朋友这几天连续经历了春天&#xff0c;初夏和冬天&#xff0c;甚至在一天中看到了雪、雨和冰雹。这变化莫测的天气让大家一时间慌了神&#xff0c;不知到底要穿些什么。由于楼上都已经停了…

php元素浮动会产生哪些影响,css浮动带来什么问题

css浮动带来的影响&#xff1a;1、由于浮动元素脱离了文档流&#xff0c;所以父元素的高度无法被撑开&#xff0c;影响了与父元素同级的元素&#xff1b;若没有给父元素设置高度&#xff0c;那么父元素就不会在显示屏上显示。2、浮动元素不再占用原文档流的位置&#xff0c;它会…

pointnet分割自己的点云数据_细嚼慢咽读论文:PointNet论文及代码详细解析

论文标题&#xff1a;PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation标签&#xff1a;有监督 | 特征学习、点云分类、语义分割首先回答3个问题作为引子&#xff1a;Q1&#xff1a;什么是点云&#xff1f;简单来说就是一堆三维点的集合&#xf…

linux将字符串转小写_小猿圈总结Linux常见命令(一)

科技发展的今天&#xff0c;互联网不断的发达&#xff0c;很多人学习Linux云计算的时候回因为记不住一些命令从而去找度娘&#xff0c;有时候因为因为找不到linux的命令而烦恼&#xff0c;下面是小猿圈linux讲师给大家总结的linux常见命令&#xff0c;希望对你有所帮助。1、cd命…

java声明arraylist,在java构造函数中声明ArrayList

我正在研究一个项目,并且我被教导在构造函数中实例化变量.我在使用ArrayList思想时遇到了一些麻烦.您能否提出一些最佳实践,我是否需要使用实例变量定义ArrayList,或者我可以在构造函数中执行此操作.谢谢你的建议&#xff01;我有一个我正在谈论的内容的例子&#xff1a;//impo…

eureka 集群失败的原因_Eureka集群的那些坑

今天遇到一个Eureka集群的一个坑。问题现场类似是这样的&#xff1a;两台Eureka组成的服务注册中心集群&#xff0c;两台服务提供方server1、server2&#xff0c;两个服务调用方client1、client2。正常的情况下&#xff1a;client1和client2通过服务中心获取的服务提供方的注册…

cnpm安装webpack_Webpack(一)介绍

一、Webpack是什么、为什么要使用它简单来说&#xff0c;Webpack是一个打包工具。站在2018年的角度&#xff0c;成为一个优秀的前端工程师&#xff0c;除了要会写页面样式和动态效果之外&#xff0c;还需要会用主流的单页面框架、Node.js、简单的前端的性能优化等等。加上现在一…

php生日验证,PHP验证生日

function pc_checkbirthdate($month, $day, $year) {$min_age 18; // 过18岁$max_age 100; // 超过122岁// 验证是不是合法时间&#xff0c;不会出现2月30号类似错误if (! checkdate ( $month, $day, $year )) {return false;}// 取得当前 年 月 日list ( $this_year, $this_…

python中的try与if,python中if和try的区别是什么

python中if和try的区别是什么发布时间&#xff1a;2020-09-10 10:04:05来源&#xff1a;亿速云阅读&#xff1a;85作者&#xff1a;小新这篇文章给大家分享的是有关python中if和try的区别是什么的内容。小编觉得挺实用的&#xff0c;因此分享给大家做个参考。一起跟随小编过来看…

bytes数组转string指定编码_一篇文章弄懂Python中所有数组数据类型

前言数组类型是各种编程语言中基本的数组结构了&#xff0c;本文来盘点下Python中各种“数组”类型的实现。listtuplearray.arraystrbytesbytearray其实把以上类型都说成是数组是不准确的。这里把数组当作一个广义的概念&#xff0c;即把列表、序列、数组都当作array-like数据类…

怎么安装php模板,PHPWind八风格模板的安装及制作教程

PHPWind八风格模板的安装及制作教程 PHPWind 8风格模板的安装及制作教程一、PHPWind风格模板的安装&#xff1a;1、下载自己喜欢的PHPWind模板&#xff0c;由于PHPWind使用者众多&#xff0c;所以为了符合多种客户的需要&#xff0c;它的风格模板也是有万千种风格&#xff0c;大…

sklearn保存svm分类模型_【菜菜的sklearn】07 支持向量机(上)

小伙伴们大家好~o(&#xffe3;▽&#xffe3;)ブ&#xff0c;我是菜菜&#xff0c;这里是我的sklearn课堂第7期&#xff0c;今天分享的内容是支持向量机&#xff08;上&#xff09;&#xff0c;下周还有下篇哦~我的开发环境是Jupyter lab&#xff0c;所用的库和版本大家参考&a…

cef在android中使用_关于富文本在Android中的应用以及遇到的坑

富文本可以为用户提供更加多样化的文本展示形式&#xff0c;但由于其使用了H5标签的特殊性&#xff0c;一般都需要第三方框架的支持。这里推荐一款合适的第三方富文本框架&#xff0c;richeditor。首先我们要使用该功能需要引入相关jar包&#xff0c;引入方法如下compile jp.wa…

thinkphp中如何使用PHP函数,如何在ThinkPHP中使用函数进行回调

如何在ThinkPHP中使用函数进行回调发布时间&#xff1a;2020-12-23 15:11:45来源&#xff1a;亿速云阅读&#xff1a;85作者&#xff1a;Leah本篇文章为大家展示了如何在ThinkPHP中使用函数进行回调&#xff0c;内容简明扼要并且容易理解&#xff0c;绝对能使你眼前一亮&#x…

unity 敌人自动攻击和寻路_Unity暑期萌新入门:环境篇

大家好&#xff0c;新一期又跟大家见面了。上一节我们完成了角色的移动控制&#xff0c;然而John只能在空白的场景中移动。因此接下来这一节我们将添加关卡、调节光照&#xff0c;让John来到阴森的鬼屋。然后设置NavMesh(导航网格&#xff0c;现在先听个概念就好)&#xff0c;为…

layui tree 加载慢_图片太多,加载慢,我用了layui里的方式,放在服务器后还是太慢!怎么解决???有没有什么优化的技巧???...

怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决怎么解决&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;layui.use([layer,flow], function(){v…

oracle 存储中文 u码,Oracle 汉字 占位

遇到了一个数据插入长度过长问题,记得大学时候,还说过oracle的不同编码下的的大小分配不是一样的,具体也忘记了,补上,以防下次犯二1 step先查看自己的oracle是什么字符集select userenv(language) from dual比如: SIMPLIFIED CHINESE_CHINA.ZHS16GBK 、 SIMPLIFIED CHINESE_CH…

element 表格宽度自适应_Java 设置Word中的表格自适应的3种方式

概述在Word创建表格时&#xff0c;可设置表格“自动调整”&#xff0c;有3种情况&#xff0c;通过Java程序设置可调用相应的方法来实现&#xff0c;即&#xff1a;根据内容调整表格AutoFitBehaviorType.Auto_Fit_To_Contents根据窗口调整表格AutoFitBehaviorType.Auto_Fit_To_W…