You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级SpringBoot 2.6与Wicket 9.8后CSV文件下载功能失效求助

问题:SpringBoot 2.1→2.6、Wicket 8.6→9.8升级后CSV下载功能失效

升级后其他功能正常,CSV文件能正常生成但无法触发下载,无报错信息,以下是相关代码及排查方向:

排查方向

  • Wicket 9.x的Ajax行为变化:Wicket 9对Ajax请求处理有调整,原AjaxFileDownload中字符串拼接的JS跳转逻辑可能因浏览器安全策略(如CSP、XSS防护)被拦截。建议改用安全的JS拼接方式:
    target.appendJavaScript(String.format("setTimeout(function(){window.location.href='%s';}, 100);", url));
    
  • 资源请求处理器调度逻辑:Wicket 9中scheduleRequestHandlerAfterCurrent的行为有变更,可尝试在onRequest中直接使用requestCycle.replaceAllRequestHandlers(handler)替代调度,同时检查Spring Security等过滤器是否拦截了回调请求。
  • 回调URL有效性验证:在浏览器开发者工具中查看网络请求,确认getCallbackUrl()生成的URL是否可访问、返回状态码是否为200,响应头是否包含Content-Disposition: attachment。
  • 文件生命周期检查:临时注释Files.remove(file)代码,确认是否因下载完成前文件被删除导致下载失败。
  • SpringBoot请求拦截配置:SpringBoot 2.6调整了WebMvc配置,检查自定义过滤器或拦截器是否阻止了Wicket的回调请求,确保/wicket/resource/...这类路径被放行。

相关代码

AjaxFileDownload类

import java.io.File;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.request.IRequestCycle;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.resource.ContentDisposition;
import org.apache.wicket.util.file.Files;
import org.apache.wicket.util.resource.FileResourceStream;
import org.apache.wicket.util.resource.IResourceStream;

// 基于Wicket官方示例实现的AJAX文件下载行为
public class AjaxFileDownload extends AbstractAjaxBehavior {
    private static final long serialVersionUID = -3721931990952363001L;

    private File file;
    private boolean deleteAfter;
    private boolean addAntiCache;

    public AjaxFileDownload() {
        this(false, true);
    }

    public AjaxFileDownload(boolean deleteAfter) {
        this(deleteAfter, true);
    }

    public AjaxFileDownload(boolean deleteAfter, boolean addAntiCache) {
        super();
        this.deleteAfter = deleteAfter;
        this.addAntiCache = addAntiCache;
    }

    // 触发下载的方法
    public void initiate(AjaxRequestTarget target) {
        if (file == null) {
            throw new IllegalArgumentException("未提供要下载的文件。");
        }
        String url = getCallbackUrl().toString();
        if (addAntiCache) {
            url = url + (url.contains("?") ? "&" : "?");
            url = url + "antiCache=" + System.currentTimeMillis();
        }

        // 延迟跳转以释放Wicket请求通道
        target.appendJavaScript("setTimeout(\"window.location.href='" + url + "'\", 100);");
    }

    @Override
    public void onRequest() {
        ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(getResourceStream(), getFileName()) {
            @Override
            public void respond(IRequestCycle requestCycle) {
                super.respond(requestCycle);

                if (deleteAfter) {
                    Files.remove(file);
                }
            }
        };
        handler.setContentDisposition(ContentDisposition.ATTACHMENT);
        getComponent().getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
    }

    private String getFileName() {
        return file.getName();
    }

    private IResourceStream getResourceStream() {
        return new FileResourceStream(new org.apache.wicket.util.file.File(file));
    }

    // 设置要下载的文件
    public void setFile(File file) {
        this.file = file;
    }
}

下载按钮点击逻辑

private AbstractButtonPanel createDownloadTemplateButton(String markupId) {
    return new ButtonPanel(markupId) {
        @Override
        protected void buttonClick(final AjaxRequestTarget target) {
            FormExceptionHandler<File> handler = dataHolder -> generateCsvTemplateFile();
            File templateDownload = handler.apply(new WicketExceptionDataholder(target, form, log));

            fileDownloadBehaviour.setFile(file);
            fileDownloadBehaviour.initiate(target);
        }

    }.withLinkTextKey("downloadTemplateButton.label");
}

ButtonPanel类

import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;

public abstract class ButtonPanel extends AbstractButtonPanel {

    public ButtonPanel(String markupId) {
        super(markupId);
    }

    @Override
    protected AjaxLink<Void> createAbstractLink(String markupId) {
        return new AjaxLink<>(markupId) {

            @Override
            public void onClick(AjaxRequestTarget target) {
                buttonClick(target);
            }
        };
    }

    protected abstract void buttonClick(AjaxRequestTarget target);

}

AbstractButtonPanel类

import com.xyz.uof.frontend.web.bootstrap.css.BootstrapCssEnum;
import com.xyz.uof.frontend.web.bootstrap.css.BootstrapCssEnum.BUTTON;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.WebComponent;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.AbstractLink;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;

public abstract class AbstractButtonPanel extends Panel {

    /**
     * 配置按钮样式(链接、主按钮等)
     */
    private String buttonSizeClass = BUTTON.SIZE_SMALL;
    private String cssClass = BootstrapCssEnum.BUTTON.PRIMARY;
    private String linkTextKey;
    private String buttonName = "submitButton"; // 默认值
    private IModel<Boolean> visibleModel = Model.of(true);
    private IModel<Boolean> enabledModel = Model.of(true);
    private String iconClass;
    private boolean spinnerActive = false;

    public AbstractButtonPanel(String markupId) {
        super(markupId);
        setOutputMarkupId(true);
    }

    @Override
    protected void onInitialize() {
        super.onInitialize();
        add(createLink("link"));
    }

    @Override
    protected void onConfigure() {
        super.onConfigure();
        setVisibilityAllowed(visibleModel.getObject());
        setEnabled(enabledModel.getObject());
    }

    private AbstractLink createLink(String markupId) {
        AbstractLink ajaxLink = createAbstractLink(markupId);
        ajaxLink.add(new AttributeAppender("class", buttonSizeClass + " " + cssClass, " "));
        ajaxLink.add(createSpinner("spinner", ajaxLink));
        ajaxLink.add(createLabel("label"));
        ajaxLink.add(createIcon("labelIcon"));
        ajaxLink.get("labelIcon").add(new AttributeAppender("class", iconClass, " "));
        return ajaxLink;
    }

    protected abstract AbstractLink createAbstractLink(String markupId);

    private Component createSpinner(String markupId, AbstractLink ajaxLink) {
        if (spinnerActive) {
            ajaxLink.add(new AttributeAppender("onClick", String.format("document.getElementById('%s').childNodes[1].className = 'spinner-grow spinner-grow-sm';", ajaxLink.getMarkupId())));
        }
        return new WebComponent(markupId) {
            @Override
            protected void onConfigure() {
                super.onConfigure();
                getParent().getId();

                setVisibilityAllowed(spinnerActive);
            }
        };
    }
    
    private Label createLabel(String markupId) {
        // 处理空链接文本的情况,支持纯图标按钮
        return new Label(markupId, () -> StringUtils.isNotBlank(linkTextKey) ? getString(linkTextKey) : "") {

            @Override
            protected void onConfigure() {
                super.onConfigure();
                setVisibilityAllowed(StringUtils.isNotBlank(linkTextKey));
            }

        };
    }

    private Component createIcon(String markupId) {
        return new WebComponent(markupId) {
            @Override
            protected void onConfigure() {
                super.onConfigure();
                setVisibilityAllowed(Optional.ofNullable(iconClass).isPresent());
            }
        };
    }

    public AbstractButtonPanel withButtonSizeClass(String buttonSizeClass) {
        this.buttonSizeClass = buttonSizeClass;
        return this;
    }

    public AbstractButtonPanel withCssClass(String cssClass) {
        this.cssClass = cssClass;
        return this;
    }

    // 供Selenium测试使用
    public AbstractButtonPanel withName(String... buttonName) {
        StringBuilder sb = new StringBuilder();
        for (String currentSplit : buttonName) {
            sb.append(currentSplit);
        }
        sb.append(this.buttonName);
        this.buttonName = sb.toString();
        return this;
    }

    public AbstractButtonPanel withLinkTextKey(String linkTextKey) {
        this.linkTextKey = linkTextKey;
        return this;
    }

    public AbstractButtonPanel withVisibleModel(IModel<Boolean> visibleModel) {
        this.visibleModel = visibleModel;
        return this;
    }

    public AbstractButtonPanel withEnabledModel(IModel<Boolean> enabledModel) {
        this.enabledModel = enabledModel;
        return this;
    }

    public AbstractButtonPanel withIconName(String iconName) {
        this.iconClass = iconName;
        return this;
    }

    public AbstractButtonPanel withSpinner(boolean spinnerActive) {
        this.spinnerActive = spinnerActive;
        return this;
    }

}

内容的提问来源于stack exchange,提问作者clatadia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 19:15:31